Monday Snack: Less JavaScript, More Native Power
Converting pixels to rems used to require a utility function in JavaScript or a preprocessor mixin. Scaling text fluidly across viewports meant combining ResizeObserver, media queries, and inline style manipulation. Every small computation that CSS couldn't handle ended up as a JS helper.
CSS has been steadily absorbing that kind of logic. Custom properties (--var), calc(), clamp(), @property, @layer each took over a piece of what used to require JavaScript. And now @function extends that trend further.
What is @function?
@function is a new CSS at-rule that lets you define your own named functions directly in CSS. These functions accept typed parameters, can include default values, and return a computed value via a result descriptor. You call them just like any other CSS function, but with a -- prefix to mark them as custom.
The rule is part of the CSS Functions and Mixins Module, which reached its First Public Working Draft on May 15, 2025. It's also listed as an experimental feature in the CSS 2026 snapshot.
If you've ever used SCSS or another preprocessor, this concept will feel familiar. Sass has had @function for years. The difference? This is native CSS. No build step, no preprocessor, evaluated by the browser's CSS engine at computed-value time.
How does it work?
The syntax is clean and straightforward. Here's the basic structure:
You define a function with a dashed name, optionally declare parameters with types and defaults, and set the return value using the result descriptor. Then you call it anywhere you'd use a value:
A few real examples show how powerful this is.
Example 1: Pixels to Rems
A classic utility: converting px to rem for better accessibility and scaling.
In the past, you either hard-coded rem values, used a preprocessor mixin, or ran a JS function on load. Now it's one line of CSS.
Example 2: Dynamic Opacity
Need a semi-transparent version of a color? Instead of hardcoding RGBA values or reaching for JavaScript:
Note the default value of 0.5 for --level. If you omit it, the function falls back gracefully.
Example 3: Fluid Typography Without Media Queries
Fluid typography is one of the most popular use cases. With @function, you can encapsulate the entire clamp() logic into a reusable function:
No media queries. No JavaScript ResizeObserver. No preprocessor build step. Just pure CSS that adapts at render time.
Example 4: Consistent Spacing Scale
Design systems rely on consistent spacing. A custom function can generate spacing values from a base unit and a multiplier, replacing what often lives in a SCSS map or a JS helper:
The default base of 0.25rem keeps things consistent, but you can override it per call when needed. Define once, reuse everywhere.
Example 5: Conditional Results with Media Queries
One of the more interesting features of @function is that you can define multiple result declarations within a single function. A media query inside the function body can override the default result, which means you can encapsulate responsive logic inside the function itself.
The first result acts as the default value. When the media query condition is met, it overrides the default with a different return value. Instead of writing separate media queries for every element that needs responsive sizing, you define the logic once and reuse it across your stylesheet. The function becomes a self-contained unit of responsive behavior.
Why this new css logic matters
The core point: when CSS handles something natively, it's almost always more efficient than doing it in JavaScript.
When you manipulate styles via JavaScript, you're working outside the browser's rendering pipeline. JS runs on the main thread, reads layout values, computes results, and writes them back, sometimes triggering reflows and repaints along the way. Each step adds overhead.
Native CSS, on the other hand, is evaluated by the browser's style engine at computed-value time. It's optimized, it's synchronous within the rendering cycle, and it doesn't block the main thread. The result? Faster rendering, lower runtime cost, and fewer jank-inducing operations.
With @function specifically, you get:
- Reusability without preprocessors or build steps
- Typed parameters that catch mismatches gracefully (invalid values fall back rather than crash)
- Default values for optional arguments
- Computed-value evaluation because the function inherits context from where it's called
- Zero JavaScript overhead because there are no listeners, no observers, no inline style injection
This means you can start removing small utility scripts and JS helpers that exist purely to compute CSS values. That's less code to maintain, fewer bugs, and better performance out of the box.
Browser support and current status
As of this year, @function is beginning to land in Chromium-based browsers. Firefox and Safari have not yet shipped implementations.
This means it's not quite ready for production as a sole solution, but it's perfect for progressive enhancement. You can wrap usage in @supports:
And fall back to a preprocessor or JS-based approach for unsupported browsers.
It's also worth noting that the spec is still evolving. The broader CSS Functions and Mixins Module includes upcoming features like @mixin and @apply, which will allow you to define reusable blocks of styles (not just single values). If @function is the first step, those are the next leaps.
Conclusion
CSS is no longer just a declarative styling language. It's becoming a capable, logic-aware layer that can handle computation, conditionals, and now custom functions. The boundary between "styling" and "logic" is shifting, and increasingly, the right place for that logic is CSS itself.
That means fewer dependencies, lighter bundles, and better performance. It also means staying curious, because the CSS you learned five years ago isn't the CSS you'll be writing tomorrow.
Give @function a try in a Chromium browser. Write a few utilities. See how it feels. And above all... have fun!