CSS bekommt eigene Funktionen: Die @function-Regel

CSS hat in den letzten Jahren ein gutes Stück des Sass-Funktionsumfangs nachgebaut. Erst Variablen über Custom Properties, dann Nesting, jetzt Funktionen und perspektivisch Mixins. Wer das Tempo verfolgt, bekommt schnell das Gefühl, CSS gerate außer Kontrolle und verwandle sich in eine Programmiersprache.
Die @function-Regel ist der jüngste Schritt. Sie erlaubt eigene Funktionen mit Parametern, Typen und einem Rückgabewert. Interessant ist daran aber nicht, dass CSS endlich kann, was Präprozessoren seit über einem Jahrzehnt können. Interessant ist, dass eine native CSS-Funktion etwas kann, das eine Sass-Funktion prinzipiell nie konnte.
Funktionen in CSS sind nicht neu, nur nativ
Sass kennt @function seit etwa zehn Jahren. Man schreibt eine Funktion, gibt mit @return einen Wert zurück, ruft sie im Stylesheet auf, und der Compiler löst das Ganze beim Build auf. Im ausgelieferten CSS ist von der Funktion nichts mehr übrig, nur das berechnete Ergebnis.
Die native Variante sieht ähnlich aus. Eine Funktion beginnt mit @function, trägt einen Namen mit zwei Bindestrichen wie eine Custom Property und liefert ihren Wert über den result-Deskriptor. Parameter werden im Körper über var() ausgelesen, ein Default steht hinter dem Doppelpunkt.
@function --space(--steps: 1) {
result: calc(var(--steps) * var(--space-unit, 0.25rem));
}
.stack {
gap: --space(4); /* 1rem */
}
.card {
padding: --space(6); /* 1.5rem */
}
Das ist eine kleine Spacing-Skala. Ein Schritt entspricht 0.25rem, --space(4) ergibt 1rem, ohne Parameter greift der Default 1. Nichts, wofür man früher nicht eine Custom Property oder eine Sass-Funktion gehabt hätte. Bis hierhin ist die Neuigkeit tatsächlich nur, dass kein Build-Step mehr nötig ist.
Typen machen die Funktion sicher
Parameter und Rückgabewert lassen sich typisieren. Der Typ steht hinter dem Parameternamen, der Rückgabetyp hinter dem returns-Keyword. Das verbessert nicht nur die Lesbarkeit, es fängt auch falsche Eingaben ab.
Der Haken liegt im Detail. Sagt man, ein Alpha-Wert müsse eine <number> sein, ist 0.15 gültig, 30% aber nicht, obwohl beide an dieser Stelle Sinn ergeben. Für solche Fälle gibt es type() mit einem | zwischen den erlaubten Typen.
@function --soft(--color <color>, --alpha type(<number> | <percentage>): 0.15)
returns <color> {
result: oklch(from var(--color) l c h / var(--alpha));
}
.menu {
box-shadow: 0 8px 24px --soft(var(--ink)); /* Default 0.15 */
outline-color: --soft(var(--primary), 30%); /* Prozent erlaubt */
}
Die Funktion nimmt eine Farbe und einen Alpha-Wert und gibt dieselbe Farbe mit verändertem Alpha zurück. Durch type(<number> | <percentage>) akzeptiert sie sowohl 0.15 als auch 30%. Übergibt man gar nichts, gilt der Default. Ohne den Union-Typ müsste man sich für eine Schreibweise entscheiden und die andere ginge verloren.
Der Unterschied, der zählt: result läuft im Browser
Hier trennt sich Native von Sass. Eine Sass-Funktion wird einmal zur Build-Zeit ausgewertet und kennt die Umgebung des Nutzers nicht. Sie weiß nicht, ob jemand im Dark Mode sitzt, auf einem schmalen Display, in einem engen Container. Sie liefert einen festen Wert, fertig.
Eine CSS-Funktion entscheidet dagegen beim Rendern. Der Funktionskörper darf Bedingungen enthalten, etwa eine Media Query, und das Ergebnis hängt davon ab, was im Moment der Darstellung zutrifft.
@function --surface() returns <color> {
result: oklch(0.98 0 0); /* hell, Standard */
@media (prefers-color-scheme: dark) {
result: oklch(0.2 0 0); /* dunkel */
}
}
.panel {
background-color: --surface();
}
Damit das funktioniert, verhält sich result bewusst anders als return in JavaScript. In JavaScript bricht die Funktion beim ersten erreichten return ab. CSS arbeitet den ganzen Körper durch, und das letzte zutreffende result gewinnt. Im Dark Mode trifft die Media Query zu, also überschreibt das zweite result das erste. Das ist exakt die Logik der Kaskade, in der spätere Deklarationen frühere schlagen, nur eben innerhalb einer Funktion.
Das lässt sich live ausprobieren. In den folgenden Demos steht links der klassische Weg, der überall läuft, rechts dieselbe Logik mit @function, die heute nur in Chromium greift. Ein Badge oben zeigt, ob dein Browser die Regel unterstützt.
Dasselbe Muster trägt auch andere Laufzeit-Entscheidungen. Eine Seitenrinne darf auf großen Viewports breiter werden, ohne dass man dafür einen zweiten Selektor und eine zweite Media Query irgendwo im Stylesheet pflegt.
@function --gutter() returns <length> {
result: 1rem;
@media (min-width: 64rem) {
result: 3rem;
}
}
.layout {
padding-inline: --gutter();
}
Genau das kann ein Präprozessor nicht. Sass müsste für jeden Fall einen festen Wert erzeugen und über getrennte Selektoren ausspielen. Die CSS-Funktion versteckt die Entscheidung an einer Stelle und reagiert live. Dasselbe Prinzip greift bei Container-Größen oder, sobald breiter verfügbar, bei if().
Ableiten statt wiederholen
Der zweite Gewinn ist unspektakulärer, aber im Alltag häufiger. Viele Werte sind keine eigenständigen Entscheidungen, sondern Ableitungen aus anderen Werten. Eine Hover-Farbe ist die Grundfarbe, etwas dunkler.
@function --darker(--color <color>, --amount <number>: 0.1) returns <color> {
result: oklch(from var(--color) calc(l - var(--amount)) c h);
}
.button {
background-color: var(--primary);
}
.button:hover {
background-color: --darker(var(--primary));
}
Die Relative-Color-Syntax oklch(from var(--color) ... ) zerlegt die Eingabefarbe in Lightness, Chroma und Hue. Hier bleibt alles gleich, nur die Lightness sinkt um den übergebenen Betrag. Der Default 0.1 greift, wenn man ohne zweiten Parameter aufruft. Die Beziehung „Hover ist etwas dunkler" liegt jetzt an einer Stelle statt an dreißig.
Dieselbe Idee löst ein bekanntes Layout-Ärgernis. Sitzt ein abgerundetes Element in einem anderen abgerundeten Element, sollte der innere Radius nicht mit dem äußeren identisch sein, sonst wirkt die Rundung schief. Die passende Formel lautet: äußerer Radius minus dem Abstand zwischen beiden Elementen, also border-radius minus padding.
@function --inner-radius(--radius <length>, --padding <length>) returns <length> {
result: max(0px, calc(var(--radius) - var(--padding)));
}
.card {
--card-radius: 1.5rem;
border-radius: var(--card-radius);
padding: 1rem;
}
.card > .thumb {
border-radius: --inner-radius(var(--card-radius), 1rem); /* 0.5rem */
}
Das max(0px, ...) verhindert, dass der Radius negativ wird, denn ein negativer Border-Radius ergibt keinen Sinn. Der äußere Radius ist ohnehin meist eine Custom Property, die seitenweit konsistent bleibt. Berechnet werden muss nur der innere, weil er vom Padding abhängt.
Mehr Macht, mehr versteckte Logik
Bleibt die Frage, ob „außer Kontrolle" ein Problem ist. Die alte Arbeitsteilung war klar: CSS beschreibt Aussehen, JavaScript trägt Logik, und was an Berechnung nötig war, erledigte ein Präprozessor zur Build-Zeit. Das Ergebnis im Browser war dummes, flaches CSS.
@function verschiebt diese Grenze. Logik, die früher im Build verschwand, wird jetzt mit ausgeliefert und im Browser ausgewertet. Das ist der Preis für die Reaktivität. Eine Funktion, die auf Dark Mode oder Container-Größe reagiert, muss zur Laufzeit existieren, nicht nur als eingefrorenes Ergebnis. Der Vorteil ist real, kein Build-Step, kein doppelter Code für hell und dunkel. Der Nachteil auch: Werte sind nicht mehr im Stylesheet ablesbar, sondern Resultat einer Auswertung, und Debugging wandert in die DevTools. Wer grep über sein CSS laufen lässt, findet den Funktionsaufruf, nicht den Wert.
Das ist keine Katastrophe, sondern ein Tradeoff, den man bewusst eingeht. CSS übernimmt Logik, die immer schon halb dazugehörte. Man sollte nur wissen, dass man sie übernimmt.
Browser-Support und Produktionseinsatz
Vor dem produktiven Einsatz steht der Realitätscheck. Die @function-Regel ist Teil des CSS Functions and Mixins Module und noch jung.
| Browser | Status (Stand Juni 2026) |
|---|---|
| Chrome / Edge (Chromium) | unterstützt ab Chrome 139 |
| Firefox | nicht unterstützt |
| Safari | nicht unterstützt |
Bei MDN gilt das Feature ausdrücklich als experimentell und ist nicht Baseline, weil es in mehreren weit verbreiteten Browsern fehlt. Für echte Projekte heißt das: nur mit Fallback. Am saubersten über die Kaskade. Man deklariert zuerst einen statischen Wert, darunter den Funktionsaufruf. Versteht der Browser die Funktion nicht, verwirft er die zweite Zeile und nutzt die erste.
.panel {
background-color: oklch(0.98 0 0); /* Fallback */
background-color: --surface(); /* mit @function */
}
Den aktuellen Stand kann man jederzeit auf caniuse prüfen.
Fazit
Dass CSS jetzt Funktionen hat, ist für sich genommen ein Nachbau dessen, was Sass lange konnte. Die eigentliche Neuigkeit ist, dass diese Funktionen im Browser entscheiden, nicht im Build. Eine Funktion, deren result auf Dark Mode oder Container-Größe reagiert, ist etwas, das ein Präprozessor strukturell nicht liefern kann. Das macht @function mehr als Komfort. Bis Firefox und Safari nachziehen, gehört es trotzdem hinter einen Fallback. Wer das Konzept jetzt versteht, ist vorbereitet, wenn der Support kommt.
Anstoß für diesen Artikel war ein Video von Fabian (Coding to Go): CSS is getting out of hand…. Syntax und Support-Angaben sind gegen MDN und das W3C-Modul gegengeprüft.