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.
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.
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 */;
}
--function-name: The name you'll use to call the function, e.g.,
--my-function().
--arg1 <type> = default: An argument. You can specify a type
(like <length> or <color>) and provide a default value.returns <type>: This is the declaration. It's
an optional keyword that tells the browser what type of value you expect the function to
output (e.g., <length>, <color>). It's the contract for what
the function provides.result: ...: This is the implementation. It's a
mandatory descriptor inside the function's body that defines the actual value the function
calculates and sends back. This is where the logic happens.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:
Since @function is experimental, you must provide fallbacks. There are two primary methods
for this.
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%);
}
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);
}
}
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);
}
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%);
}
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.
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);
}
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.
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 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.
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