CSS already has plenty of useful functions, like calc(), min(), max(), and clamp(). They let you build more flexible styles without repeating the same logic.
CSS custom functions let you take this idea a little further. The @function at-rule lets you define your own functions, pass them values, and return a result that you can use in your styles.
Syntax
Before we build anything, let's quickly look at the basic syntax:
@function --function-name(--param) { result: ...; }
The function name and parameter both start with --. The parameter is the value you pass into the function, while result defines what the function returns.
Parameters can also have a type and a default value:
@function --function-name( --param <type>: default-value ) returns <type> { result: ...; }
We'll get to types and default values later. For now, that's all you need to know.
A very simple function
Let's start with a small function that doubles a value:
@function --double(--value) { result: calc(var(--value) * 2); }
The function is called --double() and takes one parameter, --value. The result descriptor defines the value returned by the function.
You can then use it anywhere a CSS value is expected:
.box { width: --double(100px); }
Here, --double(100px) evaluates to 200px.
You can do the same thing with other bits of CSS logic. For example, you can create a function that halves a value:
@function --half(--value) { result: calc(var(--value) / 2); }
Both functions follow the same basic pattern: you pass in a value, the function does something with it, and you get a value back.
Try changing the input value and switching between --double() and --half(). The playground also includes a --limit() function, which uses clamp() to keep the result between 40px and 160px.
But why use a custom function?
You might be wondering why you need this when CSS already has calc() and custom properties.
That's a fair question. For simple calculations, you probably don't need a custom function. The useful part is being able to package a piece of CSS logic and reuse it without repeating the implementation.
For example, consider a design that uses a base spacing value and regularly needs a larger version of that value:
Instead of writing the same calculation everywhere, you can put it inside a function and call that function wherever you need it.
This becomes more useful as the logic gets a little more involved.
Passing values into a function
Custom functions can accept multiple parameters, so you can pass different inputs to the same function.
If you've ever tried to add an alpha channel to an existing hex code in pure CSS, you know it used to be a headache. Now, we can write a single function that uses relative color syntax to handle it for us:
@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }
Now, instead of manually defining new hex codes or rgba() values every time we want a see-through background, we just pass our base color and the desired alpha value to the function.
Here is a simple demo showing it in action:
The useful part here isn't the particular color conversion. It's that the implementation lives in one place, while the code using the function only needs to provide the values it cares about.
Types, defaults, and local variables
To make your functions a bit more robust, you can tell CSS what type of values each parameter expects and provide default values for parameters that are optional. You can also define custom properties inside the function that work like local variables.
@function --transparent( --color <color>, --alpha type(<number> | <percentage>): 0.8 ) { --fallback-color: transparent; /* Scoped to this function */ result: oklch(from var(--color) l c h / var(--alpha)); }
Let's break it down:
--color <color>tells CSS that the first argument must be a color.--alpha type(<number> | <percentage>): 0.8allows either a number or a percentage. If you don't provide it,0.8is used automatically.--fallback-coloris a custom property defined inside the function, so it stays scoped to the function and doesn't become available on the element calling it.
.card { background-color: --transparent(#faa6ff); } .card-dark { background-color: --transparent(#faa6ff, 50%); }
In the first example, --alpha isn't provided, so the function uses its default value of 0.8. In the second, 50% is passed explicitly and used as the alpha value.
Conditional Returns
Things get interesting when you drop @media or @container queries into a function to produce different results based on the environment.
For example, a function that returns one value on narrow screens and another on wider screens.
@function --narrow-wide(--narrow, --wide) { result: var(--wide); /* Default return */ @container (width < 500px) { result: var(--narrow); /* Overrides default if container is small */ } }
result declarations, they don't act like separate returns. The result that Try dragging the bottom-right corner of the dashed box below to resize it. Watch how the red box snaps from a fixed 300px down to 100px when the space gets tight:
Browser Compatibility
Custom CSS functions are still new, but they're an interesting step toward making CSS more reusable without moving logic into JavaScript or a preprocessor.
The best way to understand them is probably to play with the examples. Just keep an eye on browser support while you experiment.
Thanks for reading!
Check out my latest guide on how to finally style native dropdowns using appearance: base-select: HTML Selects Are Actually Styleable Now.