CSS @function At-Rule: A Complete Guide to Native CSS Custom Functions
CSS has long relied on preprocessors like Sass or Less to handle reusable logic and dynamic values. That era is changing. The native CSS @function at-rule introduces true custom functions directly into the browser, no build tools required.
In this guide, we’ll break down everything you need to know about @function: what it is, how the syntax works, real-world usage examples, and important caveats to keep in mind.
What Is the CSS @function At-Rule?
The @function at-rule lets you define reusable CSS custom functions. These functions can accept arguments, contain conditional logic, and return a value based on that logic.
Think of it as a more powerful and dynamic version of CSS custom properties (variables). While custom properties store static values, custom functions can compute and return values on the fly.
One important note: Sass also has a @function at-rule. The two are similar in purpose but different in implementation. If Sass is part of your workflow, be careful not to confuse the two when reading documentation or searching for help online.
Basic Syntax
Here is the general syntax for defining a CSS custom function:
@function --function-name(<function-parameter>#?) [returns <css-type>]? {
<declaration-rule-list>
}
At a high level, you give the function a name using a dashed identifier (like --my-function), define optional input parameters, specify an optional return type, and write the body logic inside curly braces.
Let’s break each part down further.
Function Name: The Dashed Identifier
Custom function names must start with two dashes (--), just like CSS custom properties. They are case-sensitive, meaning --conversion and --Conversion are treated as completely different functions.
@function --progression() {
/* function body */
}
Function Parameters
Parameters are optional and comma-separated. Each parameter can include:
- A name — must start with
--, for example--size - A CSS type (optional) — such as
<length>,<color>, or<number> - A default value (optional) — separated from the parameter definition with a colon (
:)
You can also declare the expected return type using returns <css-type>. This helps the browser validate your logic before rendering anything to the screen.
@function --progression(--current <number>, --total <number>) returns <percentage> {
result: calc(var(--current) / var(--total) * 100%);
}
The result Descriptor
The result descriptor is what your function actually returns. Without it, the function always returns a guaranteed-invalid value — similar to a broken custom property that produces nothing useful.
@function --half(--size <length>) {
result: calc(var(--size) / 2);
}
To use this function in your CSS:
.container {
margin-inline: --half(20px); /* Resolves to 10px */
}
Type Checking for Safer Code
One of the most powerful features of @function is built-in type checking. You can specify what types of values a parameter accepts, and the browser will reject calls that pass the wrong type.
This works the same way as type-checking in the @property at-rule, using angle brackets around the type name.
@function --progression(--current <number>, --total <number>) returns <percentage> {
result: calc(var(--current) / var(--total) * 100%);
}
.progress-bar {
width: --progression(3, 5); /* Evaluates to 60% */
}
You can also allow multiple types using a syntax combinator. Wrap the types in type() and use | as a separator:
@function --transparent(--color <color>, --alpha type(<number> | <percentage>));
This kind of type safety is invaluable in large codebases where subtle bugs can be hard to trace.
Passing Comma-Separated Lists
Since CSS uses commas to separate function arguments, passing a list of values as a single argument requires a special approach. You suffix the CSS type with # to indicate a list, and wrap the list values in curly braces when calling the function.
@function --get-range(--list <length>#, --n <length>) {
result: calc(max(var(--list)) - min(var(--list)) + var(--n));
}
div {
padding-block: --get-range({10px, 100px, 50px, 25px}, 200px); /* 290px */
}
The curly braces tell the browser to treat all the values inside as one single argument rather than multiple separate ones.
Using Conditional Logic and the CSS Cascade
The result descriptor follows the normal rules of the CSS Cascade. That means you can declare multiple result values inside conditional rules like @media, @container, or @supports, and the last valid matching value wins.
@function --suitable-font-size() returns <length> {
result: 16px;
@media (width > 1000px) {
result: 20px;
}
}
body {
font-size: --suitable-font-size();
}
Be careful about order. If the unconditional result appears after the @media block, it will always win — the media query result would never apply.
@function --suitable-font-size() returns <length> {
@media (width > 1000px) {
result: 20px; /* This will never win */
}
result: 16px; /* This always wins because it comes last */
}
Locally Scoped Custom Properties Inside Functions
You can use custom properties inside a function body. These are locally scoped and will not leak out into the global CSS environment, which keeps your code clean and predictable.
@function --spacing-scale(--multiplier) {
--base-unit: 8px;
result: calc(var(--base-unit) * var(--multiplier));
}
Nesting Functions Inside Functions
Custom functions can call other custom functions, enabling clean and modular CSS architecture where each function does exactly one job.
@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 */
}
Setting Default Parameter Values
Default values make functions more flexible. If a caller omits an argument, the default kicks in automatically. Define the default after the parameter type, separated by a colon.
@function --brand-glass(--opacity <number>: 0.5) returns <color> {
result: rgb(10 120 255 / var(--opacity));
}
.header {
background: --brand-glass(); /* Uses default: 0.5 */
}
.header:hover {
background: --brand-glass(0.8); /* Overrides to 0.8 */
}
No Side Effects: Functions Only Return Values
CSS @function is intentionally limited to returning a single value. You cannot use it to set properties on elements, generate multiple declarations, or trigger any external changes.
If you need that kind of functionality — generating blocks of CSS or applying multiple properties at once — the proposed @mixin at-rule is what you’d want. That is a separate feature still working its way through the specification process.
Circular Dependencies Are Not Allowed
CSS strictly prevents circular logic. If Function A calls Function B, and Function B calls Function A, the browser detects the cycle and marks both functions as invalid immediately.
The same rule applies to custom properties used inside functions. If a function depends on a custom property that in turn depends on that same function, the browser breaks the chain to prevent infinite recursion.
Browser Support and Progressive Enhancement
At the time of writing, @function is defined in the CSS Custom Functions and Mixins Module Level 1 specification, but browser support is still limited.
Unsupported browsers will simply ignore @function declarations. This means you can safely use progressive enhancement strategies — define fallback styles first, then layer in the custom function behavior.
You can also use @supports to detect support directly:
@supports (at-rule(@function)) {
/* Styles that rely on @function */
}
Keep in mind that the @supports at-rule() syntax itself currently only works in Chrome 148 and above. Check the current state of support before relying on this detection method in production.
Why CSS @function Matters for Modern Development
The introduction of native CSS custom functions is a significant step forward for the language. Here is why it matters:
- Less reliance on preprocessors — logic that previously required Sass or Less can now live in plain CSS
- Type safety built in — catch bugs at the CSS level rather than discovering them in the browser
- Better code reuse — write a function once and use it anywhere in your stylesheet