2026.10.22 · 5 min · css · browsers
CSS @function: write your own CSS functions.
CSS can now define named functions with typed parameters and defaults. How @function works, why it isn't a Sass function, the pitfalls that fail silently, and how to ship it while only Chromium supports it. As of 2026.10.
CSS can now define its own functions. You write the logic once, give it a name, and call it anywhere a value goes: padding: --double(8px);. No Sass, no build step. Today, reusable math in plain CSS means pasting the same calc() or oklch() formula into every rule that needs it. @function puts that formula in one place.
Watch the 60-second version on Instagram.
One formula, three copies
Say every surface that wants your brand colour see-through repeats the same relative colour formula. Three rules, three copies, and one of them with a different alpha:
.card { background: oklch(from var(--brand) l c h / 0.8); }
.badge { background: oklch(from var(--brand) l c h / 0.8); }
.modal { background: oklch(from var(--brand) l c h / 0.5); }With @function, the formula gets a name, a typed colour parameter and an alpha that defaults to 0.8:
@function --transparent(--color <color>, --alpha <number>: 0.8) returns <color> {
result: oklch(from var(--color) l c h / var(--alpha));
}
.card { background: --transparent(var(--brand)); }
.badge { background: --transparent(var(--brand)); }
.modal { background: --transparent(var(--brand), 0.5); }Change the formula in the function and all three rules follow.
How it works
The full shape is @function --name(--param <type>?: <default>?, ...) returns <type>? { result: ...; }. Piece by piece:
- The name is a dashed ident, like a custom property:
--transparent. It's case-sensitive. - Parameters behave like locally scoped custom properties. Inside the body you read them with
var(--color). - Types are optional. Leave out a parameter's type or the
returnstype and any value is accepted (type(*)). To accept more than one type, writetype(<number> | <percentage>). - Defaults come after a colon:
--alpha <number>: 0.8. resultis the return value. If there are several, the last one wins, so there's no early return. Aresultinside@mediacan override an earlier one, which gives you a responsive return value.- You call it by its bare name, with no
var()around it:padding: --double(var(--space));.
The smallest useful function is one line:
@function --double(--v) { result: calc(var(--v) * 2); }
.box { padding: --double(8px); }Defaults fill what you leave out
.card passes one argument. The function has two parameters, so --alpha falls back to its default, 0.8, and one colour comes out. .modal passes 0.5 and the default never applies.
The same fallback kicks in for an argument of the wrong type, if the parameter has a default. If it doesn't, see the first pitfall below.
It's not a Sass function, and it's not a mixin
The tempting reading is "Sass functions, now native". The difference is when it runs. A Sass function resolves at build time to a fixed value. @function runs in the browser, per element, with live custom properties and media queries. Change --brand on a subtree and every call inside it recomputes.
It's also not a mixin. A function returns one value, not a block of declarations. @mixin lives in the same spec, but it's a separate and much less stable part of it.
What people get wrong
- A wrong argument type fails silently. The value becomes invalid at computed-value time, so
background-colorends uptransparentwith no error anywhere. If the parameter has a default, an invalid argument falls back to that instead. - Commas inside an argument need braces.
--max-plus-x({1px, 7px}, 3px)passes1px, 7pxas the first argument. - No recursion. A function that calls itself is a cycle, and it returns invalid.
- It's Chromium only. Without a fallback, Firefox and Safari users get nothing.
Who supports it
| Browser | @function |
|---|---|
| Chrome and Edge 139+ | Yes, since August 2025 |
| Firefox | No, through 161 |
| Safari | Not in stable, through 27.2; it's in Safari Technology Preview |
The spec is the CSS Functions and Mixins Module (css-mixins-1); its latest Editor's Draft is dated 14 Sep 2026, and MDN labels the feature experimental, not Baseline. It has been proposed for Interop 2027 (issue #1357), but it is not selected yet: proposals closed on 23 Sep 2026 and the final scope is due in early Feb 2027. Josh Comeau put it first on his personal Interop 2027 wishlist.
So for now, put a plain declaration in front of the call, or wrap the call in @supports:
.card {
background: oklch(from var(--brand) l c h / 0.8);
background: --transparent(var(--brand));
}On a client project I'd use @function today only where a plain fallback declaration sits right in front of the call, and keep anything the layout depends on in plain CSS until Firefox and Safari ship it.
The short version
@function --name(--param <type>: <default>) returns <type> { result: ...; }defines a CSS function; call it by its bare name, novar().- It runs in the browser, per element, with live custom properties and media queries; Sass functions run at build time.
- It returns one value, not a block of declarations; that's
@mixin. - Wrong argument types fail silently, commas need
{}, and recursion returns invalid. - Chrome and Edge 139+ only; proposed for Interop 2027, not selected. Ship it behind a fallback declaration or
@supports.
Sources: MDN, @function · CSS Functions and Mixins Module Level 1 (Editor's Draft) · New in Chrome 139 · caniuse, CSS @function · Interop issue #1357, CSS @function · Microsoft Edge blog, Calling for Interop 2027 proposals · Josh W. Comeau, The Future of CSS (Interop 2027)