CSS @function is not the Sass function you know

The take going around is that the new CSS @function at-rule is Sass in the browser, so the build step can finally go. I don’t buy the second half. The name is the same and the job is similar, but the behavior differs in exactly the places where a ported function breaks quietly.

For reference, the CSS-Tricks almanac entry on @function covers the syntax, the type checking, and the cascade behavior in one place. What follows is what I’d tell another WordPress developer: what differs from Sass, what unsupported browsers do, and the ordering mistake that costs an afternoon.

Does it replace your Sass functions?

Not one for one. A Sass function runs at compile time and emits a static value, and it can loop over maps, build strings, and generate whole rule sets before anything reaches the browser. A native function runs at style resolution, per element, and it can read inherited custom properties and answer media queries, which Sass cannot do at all. That is more power in one direction. The trade is that the body returns a single value through its result descriptor, with no side effects and no way to emit several declarations. That job belongs to the still-proposed @mixin in the same specification.

So I’d split by output rather than by tool. Keep the preprocessor where you generate things: loops, map lookups, repeated rule sets. Start using @function for value math you’ve been faking with custom properties and calc(). I ran into the same native-versus-build-step question with custom media queries, and the answer held: adopt per feature rather than per stack. MDN still flags the feature as experimental, which shapes the next two answers.

Why does CSS @function ignore my media query?

Because result is a descriptor, and descriptors cascade. You can declare it more than once, and the last valid declaration wins, media queries included. The almanac’s example returns 16px by default and lifts to 20px on viewports past 1000px, and it works written this way:

@function --suitable-font-size() returns <length> {
  result: 16px;

  @media (width > 1000px) {
    result: 20px;
  }
}

Swap the two blocks, media query first and the 16px last, and the function returns 16px at every width, because the trailing declaration wins no matter what matched. No error shows up, the font never grows, and you’ll blame the media query long before you blame declaration order. Coming from Sass, this is the habit to break: a Sass body is imperative code that runs and returns, while this body is a small stylesheet the cascade sorts out.

Two smaller notes from the same corner. Custom properties declared inside the body stay local to the function, so a --base in there will not fight the global --base a page builder set on :root. And circular references, where a result depends on the function itself directly or through another function, get marked invalid instead of looping.

What happens in browsers without support?

They skip the @function rule, and a declaration that calls an unknown function is invalid, so the whole declaration is dropped. The cheap insurance is a fallback line first, which survives everywhere:

.card {
  padding-block: 1.5rem; /* fallback for everything else */
  padding-block: --spacing(3);
}

You can also gate things with @supports (at-rule(@function)), and the almanac shows that form, but it warns the check itself is new, Chrome 148 and up at the time of writing, with other browsers behind. The feature test needs its own feature test. Until that spreads, a plain fallback declaration is the version I’d ship, because it costs one line, works in every browser, and doesn’t depend on new @supports parsing. WordPress traffic still includes old WebViews and locked-down corporate browsers, and a store’s checkout is a bad place to find that out.

One caveat I haven’t verified: I’m not sure every minifier in the wild knows the at-rule yet, so compare your optimized stylesheet against the source rather than assuming they match.

Where it earns its keep on a theme

Token math. Every theme has a spacing scale, and half of them are hardcoded values.

@function --spacing(--step <number>) returns <length> {
  --base: 0.5rem;
  result: calc(var(--base) * var(--step));
}

.wp-block-columns {
  gap: --spacing(3);
}

The typed parameter is the part I like. --step is declared as a number, so a stray unit or a string invalidates the call instead of quietly producing nonsense, the same protection you get from @property. A percentage helper works the same way: --progression(3, 5) resolves to 60%, which beats calc() soup in a template partial. If you’ve already been moving this kind of math out of JavaScript, it’s the same direction. Keep the functions in the theme stylesheet or site-level Additional CSS, because theme.json presets don’t know about them yet.

Today, paste that spacing function into the Additional CSS editor under Appearance on a staging site, call --spacing(3) from one block’s gap, then load the page in a browser without support and confirm the fallback line holds. Five minutes, and you know whether your traffic can carry the pattern. If you’re weighing where native functions fit against keeping a preprocessor in a client build, I do that kind of work, so send the stylesheet if you want a second opinion.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.

Leave a Comment