Deep Dive: CSS `@function`

An interactive tutorial on the future of dynamic CSS.

Browser Support Warning

The @function at-rule is an experimental feature. Current support starts in Chrome 139+ and is mirrored in Chromium-based browsers (for example Edge and Chrome for Android). It is not yet available in Firefox or Safari.

What is `@function`?

The `@function` at-rule introduces the ability to define custom functions directly within your CSS. Think of them like JavaScript functions, but for your styles. They can take arguments, perform calculations, and return a value. This opens up a new world of possibilities for creating more maintainable, powerful, and dynamic stylesheets without relying on pre-processors like Sass.

Formal Syntax

According to the CSSWG draft, the formal syntax is more robust than a simple function. It allows for typed arguments, default values, and an explicit return type.


@function --function-name(--arg1  = default, --arg2) returns  {
  /* Function body with logic */
  result: /* some computed value */;
}

Implementation Status (Chrome and Chromium-based Browsers)

It is crucial to understand that the current implementation is experimental and does not yet support the full specification. The currently available implementation is in Chrome 139+ and browsers that mirror Chromium support. Here's a breakdown:

✅ Supported:
  • Basic function definition
  • Positional arguments
  • The `result` descriptor
❌ Not Yet Supported:
  • Typed parameters (``, etc.)
  • Default values (`--arg = 2px`)
  • The `returns` keyword
  • Named arguments when calling

Fallbacks for Unsupported Browsers

Since @function is experimental, you must provide fallbacks. There are two primary methods for this.

1. Using the Cascade

The simplest method is to rely on the CSS cascade. Define a standard CSS property first, then redefine it with your @function. Browsers that don't understand @function will simply ignore the second rule and use the first one they understood.


.element {
  /* Fallback for older browsers */
  --border-color: #cc0000;

  /* Modern browsers will use this */
  --border-color: --generate-shade(crimson, 10%);
}

2. Using a Feature Query (`@supports`)

For more complex scenarios, you can use an @supports query to check if the browser supports the function() syntax. This is the most robust method for applying styles only when the feature is available.


/* Default styles for all browsers */
.another-element {
    --padding-large: 32px;
}

/* Apply function-based styles only if @function is supported */
@supports function(--any-name(initial)) {
    .another-element {
        --padding-large: --calculate-spacing(var(--base-spacing), 2);
    }
}

Example 1: Styled Borders

Here is a function that constructs a border. Because of the current implementation status, it uses only the supported features: it requires all arguments to be passed in the correct order.


@function --create-styled-border(--width, --style, --color) {
    result: var(--width) var(--style) var(--color);
}

/* Call with all arguments in order. */
.styled-border-example-1 {
    border: --create-styled-border(2px, solid, crimson);
}

/* Call with different arguments. */
.styled-border-example-2 {
    border: --create-styled-border(5px, dashed, limegreen);
}
This box has a 2px solid crimson border.
This box has a 5px dashed limegreen border.

Example 2: The Themer

Here, we define two functions, --generate-shade and --generate-tint, to create variations of a base color. The function calculates these values once when the page first loads.


@function --generate-shade(--base-color, --percentage) {
    result: color-mix(in srgb, var(--base-color), black var(--percentage));
}

/* Usage */
.themed {
    --primary-color-dark: --generate-shade(var(--base-color), 20%);
}
Dark Shade
Base Color
Light Tint
Lighter Tint

Key Concept: Parse-Time vs. Real-Time

The reason the tints and shades don't update is fundamental to how `@function` currently works. The browser evaluates a `@function` only once, when it first parses the CSS file. It calculates the result based on the initial value of `--base-color` and then treats it as a static, fixed value.

It is not reactive. Unlike `calc()` or standard CSS variables, it does not automatically re-calculate its value when the variables it depends on are changed by JavaScript. This makes it ideal for setting up a static design system but not for building dynamic, interactive components like this themer.

Example 3: Spacing System

This example defines a --calculate-spacing function. As with the color themer, this function is only evaluated once when the CSS is parsed.


@function --calculate-spacing(--base-unit, --multiplier) {
    result: calc(var(--base-unit) * var(--multiplier));
}

.dynamic-spacing {
    --spacing-sm: --calculate-spacing(var(--base-spacing), 0.5);
    --spacing-lg: --calculate-spacing(var(--base-spacing), 2);
}
1rem (16px)
Small Margin Bottom
Medium Padding
Large Margin
Extra Large Padding

Key Concept: Parse-Time vs. Real-Time

This example demonstrates the same principle as the themer. Even though the function's result contains `calc()`, the entire function is resolved to a static value (e.g., `8px`) during the initial CSS parsing. It does not remain a "live" calculation.

Example 4: Fluid Typography

Our --fluid-typography function makes responsive text easy. The interactivity here comes from the browser itself. To see it work, resize the container by dragging its bottom-right corner, or resize your entire browser window.


@function --fluid-typography(--min-size, --max-size, --min-vw, --max-vw) {
    result: clamp(var(--min-size),
                   calc(var(--min-size) + (var(--max-size) - var(--min-size)) * ((100vw - var(--min-vw)) / (var(--max-vw) - var(--min-vw)))),
                   var(--max-size));
}

This text scales fluidly!

Key Concept: Returning a Dynamic Function

This example is interactive because the `@function` returns a `clamp()` expression that contains `vw` (viewport width) units. The browser is built to re-evaluate functions like `clamp()` and `calc()` whenever the viewport size changes.

The `@function` is still only parsed once, but the complex `clamp()` expression it produces remains "live" and responsive to browser resizing. This is a perfect use case for `@function`: simplifying complex, but inherently dynamic, CSS logic.

Conclusion

The @function at-rule provides new avenues to writing complex styles without using JavaScript but, as with everything web-related, it's not a silve bullet.

It has the the same problem as SASS functions: It's only ran when the stylesheet is first parsed. The difference is that CSS @function requires a page reload to parse the stylesheet again while SASS functions require recompilation and reload.

With all its problems, CSS @functions provide a good abstraction for complex, dynamic CSS logic