You know that one calc() you keep copy-pasting? The one with three nested var()s, a magic number, and a comment that says /* don't touch */? It lives in eleven places in your stylesheet, and every one of them is a slightly different mutation. CSS finally has a cure: CSS custom functions. You write the logic once with the @function at-rule, give it a name like --fluid-type(), and call it anywhere a value goes. No Sass build step, no JavaScript, no /* don't touch */.
When this draft was first written, custom functions were a spec-shaped rumor. They’re not anymore: Chrome shipped @function in version 139, and the syntax has settled enough to learn for real. Here’s how they work, where they’re supported, and how to use them today without breaking the site for everyone on Firefox.
What Are CSS Custom Functions?
A CSS custom function is a reusable recipe that takes inputs and returns a value. If CSS custom properties are labeled jars in your pantry (--brand: #7b2ff7), a custom function is the recipe card taped inside the cabinet door. The jar holds one thing. The recipe card says, “give me any color and an amount, and I’ll hand you back a perfectly tinted version.” Same kitchen, way less chopping.
They’re defined in the CSS Working Group’s CSS Functions and Mixins Module, which is still an Editor’s Draft. Here’s the smallest useful one, straight from the shipped syntax:
Basic @function:
@function --double(--value) {
result: calc(var(--value) * 2);
}
.card {
--space: 12px;
padding: --double(var(--space)); /* 24px */
}
Three things to notice. The function name starts with two dashes, just like a custom property. Parameters are custom properties too (--value), which you read inside the body with plain old var(). And there’s no return keyword: the output is set with the result descriptor. If you learned this from the old speculative examples floating around, with $params and return, toss that recipe card. That was Sass cosplay.
Anatomy of a Recipe Card
Real recipes specify ingredients precisely, and so can you. Parameters can declare a type, a default value, and the function itself can declare what it returns:
Typed parameters, defaults, and return type:
@function --tint(--color <color>, --amount <percentage>: 20%) returns <color> {
result: color-mix(in oklch, var(--color), white var(--amount));
}
.button {
--brand: #7b2ff7;
background: --tint(var(--brand)); /* uses the 20% default */
border-color: --tint(var(--brand), 45%); /* overrides it */
}
The type sits right after the parameter name (<color>, <length>, <number>, and friends), the default comes after a colon, and returns goes before the opening brace. Need to accept more than one type? Use type(), as in --alpha type(<number> | <percentage>). Per MDN, if you pass an invalid argument to a parameter that has a default, the default is used instead. Pass a bad one to a parameter without a default, and the whole value goes invalid at computed-value time.
You can also stash locally scoped custom properties inside the body as constants. They stay in the function, like the secret ingredient Grandma never wrote down.
Local constants:
@function --anim-1s(--name, --count: 1) {
--duration: 1s;
--easing: ease-out;
result: var(--name) var(--duration) var(--count) var(--easing);
}
.badge { animation: --anim-1s(pop, 2); }
Logic Inside: @media and if()
Here’s where it stops being fancy calc(). A function body can contain conditional at-rules like @media, and you can write several result descriptors. Normal cascade rules apply: the last one that applies wins. There are no early returns, so put your default first and the overrides after.
Responsive result with @media:
@function --narrow-wide(--narrow, --wide) {
result: var(--wide);
@media (width < 700px) {
result: var(--narrow);
}
}
.layout {
grid-template-columns: --narrow-wide(1fr, 2fr 1fr);
}
That’s a media query tucked inside a value, which pairs beautifully with CSS Grid layouts. The same idea can be written with the new inline if() function, which Chrome shipped in version 137:
Same thing with if():
@function --narrow-wide(--narrow, --wide) {
result: if(media(width < 700px): var(--narrow); else: var(--wide));
}
Note the syntax: condition, colon, value, then a semicolon before the next branch. No space between if and the parenthesis. if() can also test supports() and style() queries, which makes it a handy partner for theme switches like light and dark modes.
Recipes Worth Taping to the Cabinet
A few functions that earn their keep on real projects:
Fluid type helper:
@function --fluid(--min <length>, --max <length>, --vw <number>: 4) returns <length> {
result: clamp(var(--min), calc(var(--min) + var(--vw) * 1vw), var(--max));
}
h1 { font-size: --fluid(2rem, 3.5rem); }
h2 { font-size: --fluid(1.5rem, 2.25rem, 3); }
That wraps clamp() so your responsive typography scale reads like a spec sheet instead of an algebra exam.
Transparent brand colors:
@function --alpha(--color <color>, --a <number>: 0.5) returns <color> {
result: oklch(from var(--color) l c h / var(--a));
}
.card {
box-shadow: 0 12px 32px --alpha(var(--brand), 0.25);
}
That one uses relative color syntax to make any token see-through, which is perfect for tinted CSS shadows that match your color palette instead of muddy gray.
Where Browser Support Stands
As of late October 2026, according to MDN’s browser compatibility data:
- Chrome and Edge:
@functionsupported since version 139;if()since 137. - Safari:
@functionis in Safari Technology Preview only;if()is not supported yet. - Firefox: neither
@functionnorif()is supported yet.
MDN still labels @function experimental and “Limited availability,” meaning it isn’t Baseline. And the “Mixins” half of the module, @mixin and @apply, is still being worked out in the draft spec and hasn’t shipped in any stable browser. So: real, usable, but not something every visitor gets. Time for some progressive enhancement.
Use Them Today Without Breaking Anything
The simplest fallback is the oldest trick in CSS: write the boring value first, then the fancy one. Browsers that don’t understand a dashed function treat that declaration as invalid and skip it, keeping the value above.
Cascade fallback:
.button {
background: #9a5cf9; /* everyone */
background: --tint(var(--brand), 20%); /* browsers with @function */
}
For bigger chunks of styling, gate them behind @supports using a feature that shipped alongside functions. Since Chrome shipped if() before @function, testing for if() alone doesn’t prove functions work, so the per-declaration fallback above stays your safety net either way:
@supports gate for if():
@supports (color: if(else: red)) {
.layout {
grid-template-columns: if(media(width < 700px): 1fr; else: 2fr 1fr);
}
}
Keep core layout and readability on the baseline path. Let functions polish things: nicer tints, smoother type scales, fewer repeated values. If the polish disappears in Firefox, nobody should notice anything except slightly less sparkle.
Pitfalls to Watch For
- Silent failure. In supporting browsers, a type mismatch makes the value invalid at computed-value time, so you get the property’s initial or inherited value, not your fallback line. Check DevTools when a color suddenly goes transparent.
- Order of results. Last applicable
resultwins. Put the default first, conditions after. - Commas in arguments. Passing a list like
1px, 7px, 2pxas one argument? Wrap it in curly braces:--max-plus-x({1px, 7px, 2px}, 3px). - Scope surprises. Functions can see custom properties on the element where they’re called, and parameters override those. Name things clearly so
--sizedoesn’t mean three things. - Name collisions. If two functions share a name, the one in the stronger cascade layer wins, then the later one in source order.
- Not Sass. Functions run in the browser at computed-value time, per element. That’s the superpower (they react to the live cascade), but don’t expect loops, string building, or compile-time tricks.
Make It Function
CSS custom functions turn your stylesheet’s repeated math into named, typed, reusable recipes, and they’re already live in Chromium browsers. Learn the real syntax (@function, --name(), result), lean on fallbacks, and keep an eye on Safari and Firefox as they catch up. Your future self, the one who no longer has to edit eleven calc()s, says thanks.
Building a new site and want it engineered with modern, maintainable CSS from day one? Get a custom web design quote and let’s cook up something great.