Experimental: Check browser support before using this in production.

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. If that condition matches, we apply styles (<declaration-rule-list>).

Arguments and Descriptors

There are a number of parts to the syntax for @function to handle different aspects of the feature. It may look complex, but it becomes clearer when looking at 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 when the argument is omitted during the function call. If you provide a default value, it must be valid for 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, anything will be valid (equivalent to writing returns type(*)).
  • <declaration-rule-list>: CSS declarations and at-rules that construct the function’s body and logic. It can include custom properties and the result descriptor — either at the root or nested within an at-rule.
@function --progression(--current <number>, --total <number>) returns <percentage> {
  result:
}

The result descriptor defines what the custom function will return. If a custom function omits the result descriptor, it will always return a guaranteed-invalid value, just like a broken custom property.

@function --progression(--current <number>, --total <number>) returns <percentage> {}

Basic Usage

The following example shows a basic function that takes a provided value (e.g., 20px), halves it (e.g., 10px), and returns it as a length unit:

@function --half(--size <length>) {
  result: calc(var(--size) / 2);
}

Here, the function-token is set to --half. A function-parameter called --size is created with its css-type set to <length>, so it only accepts length values. The result descriptor is set to calc(--size / 2), which uses the CSS calc() function to halve the value sourced from the --size parameter.

The function is then called like so:

.container {
  margin-inline: --half(20px); /* This will resolve to 10px */
}

Type Checking

Just like in JavaScript or other languages, sometimes you want to ensure a function only accepts certain arguments. For example, to ensure only numbers can be input and 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 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 raises the question: what if you want 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, suffix the # character to the <css-type>. When calling the function, wrap the list of values in curly braces, which tells the browser to treat everything inside the braces as a single argument.

/* 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 property. Conditional group rules (@media, @container, @supports) and other functions such as if() provide a lot of additional possibilities.

In the following example, --suitable-font-size defaults to 16px when the screen is less than 1000px. If the screen is greater than 1000px, the “winning” style is what’s inside 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. In the example below, the result would always be 16px, regardless of whether the media query is triggered.

@function --suitable-font-size() returns <length> {
  @media (width > 1000px) {
    result: 20px;
  }

  result: 16px;
}

The cascade also allows you to use custom properties within a function. These custom properties are locally scoped — accessible only within your custom function and any custom function that references it — so they 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 inside another. This allows for clean, modular code where each function does one job and 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. A 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 a function or use a function to generate multiple declarations. The feature is intentionally constrained to pure value computation, keeping CSS functions predictable and free of unintended consequences.