The @function at-rule defines CSS custom functions. These custom functions are reusable blocks of CSS that can accept arguments, contain complex logic, and return values based on that logic. The feature is similar in nature to a more dynamic version of custom properties (CSS variables).
Note: There is also a @function at-rule in Sass which is similar in purpose but different in function to the native CSS @function. Be aware of this if Sass is part of your stack or when searching for resources as it is easy to conflate one with the other.
Syntax
The @function at-rule defines a custom function, using the following syntax:
@function --function-name(<function-parameter>#?) [returns <css-type>]? {
<declaration-rule-list>
}
<function-parameter> = <custom-property-name> <css-type>? [ : <default-value> ]?
In other words, we define the function’s name as a dashed ident (--my-function), supply some condition we want to match (<function-parameter>), and say what sort of thing we want to return, say, a CSS[<length>] value. And, if that condition matches, we apply styles (<declaration-rule-list>).
Let’s dig deeper into what those things actually mean.
Arguments and Descriptors
There are a number of parts to the syntax for @function to handle different parts of the feature. It may all look very complex — and it is — but it’ll become clearer later when we look at some examples.
--function-token
A user-defined identifier that must start with two dashes (--), similar to the dashed-ident of custom properties. Just like custom properties, the name is case-sensitive. For example, --conversion and --Conversion would refer to different custom function definitions.
@function --progression()
<function-parameter> (optional)
An optional comma-separated list of inputs that can include:
--param-name: The name of the argument (must start with--).<css-type>(optional): A keyword or type (e.g.,<length>,<color>) that tells the function what sort of input or result it’s returning when it hits a matched condition.<default-value>(optional): A fallback value that’s returned if the result is invalid, such as the argument is omitted during the function call. If you provide a default value, it must be valid to the aforementioned<css-type>(e.g. a<length>must default to a valid CSS length). It is separated from the rest of the parameter definition with a colon (:).returns <css-type>(optional): Defines the expected output type of the function. This helps the browser validate logic before rendering. If a type isn’t specified then, anything will be valid (like writingreturns type(*)).<declaration-rule-list>: CSS declarations and at-rules that construct the function’s body and logic. It can include custom properties and theresultdescriptor — either at the root or nested within an at-rule.
@function --progression(--current <number>, --total <number>) returns <percentage> {
result:
}
The result descriptor that defines what the custom function will return. If a custom function forgoes the result descriptor, it will always return guaranteed-invalid value, just like a broken custom property.
@function --progression(--current <number>, --total <number>) returns <percentage> {}
Basic Usage
For an example of the most basic function you could make, we have a function that calculates a provided value (e.g. 20px) in half (e.g. 10px), and returns returns it as a length unit (e.g. px):
@function --half(--size <length>) {
result: calc(var(--size) / 2);
}
Here, we are ‘naming’ our function by setting the function-token to --half. We are then creating a function-parameter called --size, and setting the css-type to <length>, so that it will only accept length values. The result descriptor is set to calc(--size / 2), which uses the CSS Calculating Function to halve the value sourced from the size function-parameter.
We then use the function like so:
.container {
margin-inline: --half(20px); /* This will resolve to 10px */
}
Type Checking
Just like when writing JavaScript or other languages, sometimes we want to ensure a function only accepts certain arguments. For example, what if we want to ensure that only numbers can be input and that only a percentage can be output?
@function --progression(--current <number>, --total <number>) returns <percentage> {
result: calc(var(--current) / var(--total) * 100%);
}
.progress-bar {
width: --progression(3, 5); /* Evaluates to 60% */
}
The <css-type> is enclosed in angle brackets in a manner identical to how you type-check a custom property via @property. If an argument does not match the declared type (e.g. <color>), the function call becomes invalid, which is very valuable for catching bugs early in large codebases.
You can use a <syntax-combinator> to allow multiple types by wrapping the types in type() and using | as a separator. For example, --alpha here allows both <number> and <percentage>:
@function --transparent(--color <color>, --alpha type(<number> | <percentage>));
Comma-Separated Lists
CSS uses commas to separate the inputs of a custom function, which begs the question: what if you wish to provide a list of values? To provide a list of values as one input rather than several separate inputs, you must first mark a function to expect a list.
To do this, you suffix the # character to the <css-type>. When calling the function, you then wrap the list of values in curly braces, which tells the browser to treat everything inside the braces as a single argument.
For instance:
/* Calculates the distance between the highest and lowest values in a list, plus another input */
@function --get-range(--list <length>#, --n <length>) {
result: calc(max(var(--list)) - min(var(--list)) + var(--n));
}
div {
/* Finds the difference between 10px and 100px, then adds 200px */
padding-block: --get-range({10px, 100px, 50px, 25px}, 200px); /* 290px */
}
Constructs and the CSS Cascade
The result descriptor follows the rules of the CSS Cascade. That means you can declare multiple result values, and the last valid matching value will win, just like any other properties. As such, conditional group rules (@media, @container, @supports) and other functions, such as if(), provide a lot of additional possibilities.
In this case, we return a --suitable-font-size that defaults to 16px when the screen is less than 1000px pixels. If the screen is greater than 1000px then the “winning” style is what’s in the @media block.
@function --suitable-font-size() returns <length> {
result: 16px;
@media (width > 1000px) {
result: 20px;
}
}
body {
font-size: --suitable-font-size();
}
Keep in mind that the last defined value always wins, so if you were to write the example below, the result would always be 16px, regardless of the media query being triggered.
@function --suitable-font-size() returns <length> {
@media (width > 1000px) {
result: 20px;
}
result: 16px;
}
The adherence to the established cascade also allows you to use custom properties within a function. These custom properties are locally scoped, so they are only accessible in your custom function and any custom function that references it and thus won’t unexpectedly leak out globally and interact with the rest of your CSS.
@function --spacing-scale(--multiplier) {
--base-unit: 8px;
result: calc(var(--base-unit) * var(--multiplier));
}
You can also use other custom functions within a custom function, essentially nesting one function within another. This allows for very clean code, where each section only does one job and functions can be widely reused.
@function --square(--n) {
result: calc(var(--n) * var(--n));
}
@function --circle-area(--radius) {
--pi: 3.14159;
result: calc(var(--pi) * --square(var(--radius)));
}
.blob {
width: calc(--circle-area(10) * 1px); /* 314.159px */
}
Defaults
Functions can handle multiple arguments and provide default values. The default value is defined by including it at the end of the function parameter, separated by a colon (:).
/* Define the function */
@function --brand-glass(--opacity <number>: 0.5) returns <color> {
result: rgb(10 120 255 / var(--opacity));
}
/* Use the function */
.header {
background: --brand-glass(); /* Defaults to 0.5 */
}
.header:hover {
background: --brand-glass(0.8); /* Overrides to 0.8 */
}
No Side Effects
A CSS @function can only return a value; it cannot do anything else. For example, you cannot change a property inside of a function or use a function to generate multiple declarations. For such abilities, one must look to the proposed @mixin at-rule, which would provide functionality in this manner, allowing multiple lines of CSS properties and other complex logic.
Circular Dependencies
CSS is very strict about circular logic. If Function A calls Function B, and Function B calls Function A, the browser will catch this cyclic dependency and immediately mark both as invalid.
This also applies to CSS Custom Properties and referring to the custom function itself. If a function relies on a custom property or function that is itself calculated by that same function, the browser will end the calculation to prevent an infinite recursion.
Specification
The @function at-rule is defined in the CSS Custom Functions and Mixins Module Level 1 specification.
Browser Support
Unsupported browsers ignore @function, so fallback declarations and progressive enhancement strategies can be advantageous. You can use @supports to check if @function is supported in the user’s browser, like so:
@supports (at-rule(@function)) {
/* ... */
}
Ironically, however, at time of writing, the @supports at-rule evaluation functionality doesn’t have full support across browsers (Chrome 148+ only), so you will need to check if it is supported in your case. You can see the discussion on this in CSS Drafts Issue #2463.
More Information
@function originally handwritten and published with love on CSS-Tricks. You should really get the newsletter as well.
Source: CSS-Tricks