CSS Custom Functions and Mixins Module Level 1

Editor’s Draft,

More details about this document
This version:
https://drafts.csswg.org/css-mixins/
Latest published version:
https://www.w3.org/TR/css-mixins-1/
Feedback:
CSSWG Issues Repository
Inline In Spec
Editors:
Miriam E. Suzanne (Invited Expert)
Tab Atkins-Bittner (Google)
Suggest an Edit for this Spec:
GitHub Editor
Test Suite:
https://wpt.fyi/results/css/css-mixins/

Abstract

This module defines the ability for authors to define custom functions, acting similar to parametrized custom properties. They can use the full power of CSS’s values and conditional rules. It also defines an early form of a similar idea for CSS rule mixins, allowing parametrized substitution of entire blocks of properties into other rules.

CSS is a language for describing the rendering of structured documents (such as HTML and XML) on screen, on paper, etc.

Status of this document

This is a public copy of the editors’ draft. It is provided for discussion only and may change at any moment. Its publication here does not imply endorsement of its contents by W3C. Don’t cite this document other than as work in progress.

Please send feedback by filing issues in GitHub (preferred), including the spec code “css-mixins” in the title, like this: “[css-mixins] …summary of comment…”. All issues and comments are archived. Alternately, feedback can be sent to the (archived) public mailing list www-style@w3.org.

This document is governed by the 18 August 2025 W3C Process Document.

1. Introduction

This section is not normative.

Note: At this time, this specification only defines custom functions, which operate at the level of CSS values. It is expected that it will define "mixins" later, which are functions that operate at the style rule level.

Custom properties give authors a lot of power to define useful, sometimes complex values in one place, and then re-use them across their stylesheet. They can vary across the document, or based on Media Queries or other conditionals, making them very flexible and responsive.

However, their values are fixed at the point they’re defined, unable to be changed except by fully overriding their previous definition: a --shadow: 2px 2px var(--shadow-color) declaration takes its --shadow-color value from the element it’s declared on, and later changes to --shadow-color on descendant elements don’t alter the value of --shadow for them; they continue to use the shadow color defined where --shadow was defined. This is a common source of confusion for authors making heavy use of composite variables like this.

Custom functions allow authors the same power as custom properties, but parameterized: they have the same flexibility and conditionality as a custom property definition, but take values from other custom properties (or explicitly as arguments) at the point of use. For example, instead of a --shadow custom property, a --shadow() custom function could be defined instead, like:

@function --shadow(--shadow-color <color> : inherit) {
  /* If --shadow-color argument isn't passed,
     or doesn't successfully parse as a <color>,
     try to use the --shadow-color *property*
     from the element instead */

  /* var(--shadow-color) refers to the --shadow-color parameter,
     rather than a custom property,
     but can still use a fallback value as normal */
  result: 2px 2px var(--shadow-color, black);
}

.foo {
  --shadow-color: blue;
  box-shadow: --shadow(); /* produces a blue shadow */
  /* or just */
  box-shadow: --shadow(blue);
}
Tests

2. Defining Custom Functions

A custom function can be thought of as an advanced custom property, which instead of being substituted by a single fixed value, computes its substitution value based on function parameters and the value of custom properties at the point it’s invoked. Rather than the var() syntax that custom properties use for substitution, custom functions are invoked by <dashed-function> syntax, allowing additional values to be passed as arguments.

A simple custom function to negate a value can be defined as follows:
@function --negative(--value) {
  result: calc(-1 * var(--value));
}

Then, that function can be referenced with --negative() in some declaration:

html {
  --gap: 1em;
  padding: --negative(var(--gap));
  /* or by passing the value explicitly, like: */
  padding: --negative(1em);
}

<dashed-function>s are arbitrary substitution functions, like var(). Their presence in a property’s value causes it to be assumed valid at parse time, and only evaluated and parsed at computed-value time, after arbitrary substitution has occurred.

2.1. The @function Rule

The @function rule defines a custom function, and consists of a name, a list of parameters, a function body, and optionally a return type described by a syntax definition.

Each function parameter consists of a name (<custom-property-name>); optionally a parameter type, described by a syntax definition; and optionally a default value.

<@function> = @function <function-token> <function-parameter>#? )
  [ returns <css-type> ]?
{
  <declaration-rule-list>
}

<function-parameter> = <custom-property-name> <css-type>? [ : <default-value> ]?
<css-type> = <syntax-component> | <type()>
<default-value> = <declaration-value>
<type()> = type( <syntax> )
Tests

If a default value and a parameter type are both provided, then the default value must parse successfully according to that parameter type’s syntax. Otherwise, the @function rule is invalid.

2.1.1. The Function Prelude

The <function-token> production must start with two dashes (U+002D HYPHEN-MINUS), similar to <dashed-ident>, or else the definition is invalid.

The name of the resulting custom function is given by the name of the <function-token>. The optional function parameters are given by the <function-parameter> values (defaulting to an empty set), and the optional return type is given by the <css-type> following the returns keyword (defaulting to type(*)).

If the <css-type> of a function parameter or return type can be described by a single <syntax-component>, then the type() function can be omitted:
@function --foo(--a <length>) { /* ... */ }
@function --foo(--a <color>) { /* ... */ }
@function --foo(--a <length>+) { /* ... */ }

However, any <syntax> that requires a <syntax-combinator> needs to be wrapped in the type() function:

@function --foo(--a type(<number> | <percentage>)) { /* ... */ }

The name of a @function rule is a tree-scoped name. If more than one @function exists for a given name, then the rule in the stronger cascade layer wins, and rules defined later win within the same layer.

If the function parameters contain the same <custom-property-name> more than once, then the @function rule is invalid.

2.1.2. The Function Body

The body of a @function rule accepts conditional group rules, such as @media. Additionally, it accepts the following descriptors:

Unknown descriptors are invalid and ignored, but do not make the @function rule itself invalid.

2.2. The result Descriptor

Name: result
For: @function
Value: <declaration-value>?
Initial: n/a (see prose)

The result descriptor defines the result of evaluating the custom function defined by its @function rule. Using var() functions, it can reference function parameters, local variables, as well as other custom functions via <dashed-function>s.

The result descriptor itself does not have a type, but its resolved value is type-checked during the substitution of a <dashed-function>.

2.3. Arguments & Local Variables

This section is non-normative.

Within a custom function’s function body, the var() function can access local variables (the custom properties defined in the function body), function parameters (the values passed to the function, or set to default values), and custom properties defined at the call site (an element, or another custom function).

In that list, earlier things "win" over later things of the same name—​if you have a local variable named --foo, var(--foo) will be substituted by that local variable, not by an argument or a custom property defined outside. The other values can still be accessed, however: setting the --foo local variable to initial will resolve it to the --foo parameter, while inherit will resolve it to the --foo custom property from the call site.

A custom function can access local variables and function parameters from functions higher up in the call stack:
@function --outer(--outer-arg) {
  --outer-local: 2;
  result: --inner();
}
@function --inner() returns <number> {
  result: calc(var(--outer-arg) + var(--outer-local));
}
div {
  z-index: --outer(1); /* 3 */
}

Similarly, custom properties are implicitly available:

@function --double-z() returns <number> {
  result: calc(var(--z) * 2);
}
div {
  --z: 3;
  z-index: --double-z(); /* 6 */
}

But function parameters "shadow" custom properties, and local variables "shadow" both:

@function --add-a-b-c(--b, --c) {
  --c: 300;
  result: calc(var(--a) + var(--b) + var(--c));
  /* uses the --a from the call site's custom property,
     the --b from the function parameter,
     and the --c from the local variable */
}
div {
  --a: 1;
  --b: 2;
  --c: 3;
  z-index: --add-a-b-c(20, 30); /* 321 */
}

3. Using Custom Functions

Similar to how the value of a custom property can be substituted into the value of another property with var(), the result of a custom function evaluation can be substituted into the value of a property with a <dashed-function>.

A <dashed-function> is a functional notation whose function name starts with two dashes (U+002D HYPHEN-MINUS). Its argument grammar is:

<dashed-function> = --*( <declaration-value>#? )

A <dashed-function> can only be used where var() is allowed.

If a property contains one or more <dashed-function>s, the entire property’s grammar must be assumed to be valid at parse time. At computed-value time, every <dashed-function> must be replaced before finally being checked against the property’s grammar.

Note: Within the body of a custom function, var() functions might resolve differently than on the element the <dashed-function> is used on. See § 3.1 Evaluating Custom Functions.

A <dashed-function> is evaluated in some context: either in a property value on an element (or in a descriptor that is eventually treated like a property on an element, such as in @keyframes), or in a descriptor in the function body of another custom function that is being applied to a "hypothetical" element. Either way, this provides a calling context, which contains the property or descriptor name containing the <dashed-function>, and the element (or "hypothetical" element) that property/descriptor is being applied to.

As calling contexts are nested by <dashed-function> evaluations inside of custom functions, a calling context’s root element is the real element at the root of the calling context stack.

To replace a dashed function dashed function, with a list of arguments:
  1. Let function be the result of dereferencing the dashed function’s name as a tree-scoped reference. If no such name exists, return the guaranteed-invalid value.

  2. For each arg in arguments, substitute arbitrary substitution functions in arg, and replace arg with the result.

    Note: This may leave some (or all) arguments as the guaranteed-invalid value, triggering default values (if any).

  3. If dashed function is being substituted into a property on an element, let calling context be a calling context with that element and that property

    Otherwise, it’s being substituted into a descriptor on a "hypothetical element", while evaluating another custom function. Let calling context be a calling context with that "hypothetical element" and that descriptor.

  4. Evaluate a custom function, using function, arguments, and calling context, and return the equivalent token sequence of the value resulting from the evaluation.

Tests
A comma-containing value can be passed as a single argument by wrapping the value in curly braces, {}:
@function --max-plus-x(--list, --x) {
  result: calc(max(var(--list)) + var(--x));
}
div {
  width: --max-plus-x({ 1px, 7px, 2px }, 3px); /* 10px */
}
In the following, --foo() is in a cycle with itself:
@function --foo(--x) {
  result: --foo(10);
}

Similarly, --bar() is in a cycle with itself, even though the local variable --x is never referenced by result:

@function --bar() {
  --x: --bar();
  result: 1;
}

However, --baz() is not in a cycle with itself here, since we never evaluate the result declaration within the @media rule:

@function --baz(--x) {
  @media (unknown-feature) {
    result: --baz(42);
  }
  result: 1;
}
The function --baz() is not in a cycle in the example below: even though var(--x) and var(--y) appear in the function body, they refer to a function parameter and local variable, respectively. The custom properties --x and --y both reference --baz(), but that’s fine: those custom properties are not referenced within --baz().
@function --baz(--x) {
  --y: 10px;
  result: calc(var(--x) + var(--y));
}

div {
  --x: --baz(1px);
  --y: --baz(2px);
  width: var(--x);  /* 11px */
  height: var(--y); /* 12px */
}

3.1. Evaluating Custom Functions

Custom functions are evaluated by, essentially, pretending their function body is a style rule being applied to a hypothetical element, resolving styles as normal, and then returning the value of the result descriptor on that hypothetical element. The hypothetical element "inherits" the values of all custom properties as if it were a child of its calling context, with its function parameters overriding "inherited" custom properties of the same name.

To evaluate a custom function custom function, given a calling context calling context and a list of CSS values arguments, returning a CSS value:
  1. Let substitution context be a substitution context containing «"function", custom function».

    Note: Due to tree-scoping, the same function name may appear multiple times on the stack while referring to different custom functions. For this reason, the custom function itself is included in the substitution context, not just its name.

  2. Guard substitution context for the remainder of this algorithm. If substitution context is marked as cyclic, return the guaranteed-invalid value.

  3. If the number of items in arguments is greater than the number of function parameters in custom function, return the guaranteed-invalid value.

  4. Let registrations be an initially empty set of custom property registrations.

  5. For each function parameter of custom function, create a custom property registration with the parameter’s name, a syntax of the parameter type, an inherit flag of "true", and no initial value. Add the registration to registrations.

  6. If custom function has a return type, create a custom property registration with the name "result" (violating the usual rules for what a registration’s name can be), a syntax of the return type, an inherit flag of "false", and no initial value. Add the registration to registrations.

  7. Let argument rule be an initially empty style rule.

  8. For each function parameter of custom function:

    1. Let arg value be the value of the corresponding argument in arguments, or the guaranteed-invalid value if there is no corresponding argument.

    2. Let default value be the parameter’s default value.

    3. Add a custom property to argument rule with a name of the parameter’s name, and a value of first-valid(arg value, default value).

  9. Resolve function styles using custom function, argument rule, registrations, and calling context. Let argument styles be the result.

  10. Let body rule be the function body of custom function, as a style rule.

  11. For each custom property registration of registrations except the registration with the name "result", set its initial value to the corresponding value in argument styles, set its syntax to the universal syntax definition, and prepend a custom property to body rule with the property name and value in argument styles.

  12. Resolve function styles using custom function, body rule, registrations, and calling context. Let body styles be the result.

  13. If substitution context is marked as a cyclic substitution context, return the guaranteed-invalid value.

    Note: Nested arbitrary substitution functions may have marked substitution context as cyclic at some point after step 2, for example when resolving result.

  14. Return the value of the result property in body styles.

Tests
To resolve function styles, given a custom function custom function, a style rule rule, a set of custom property registrations registrations, and a calling context calling context, returning a set of computed styles:
  1. Create a "hypothetical element" el that acts as a child of calling context’s element. el is featureless, and only custom properties and the result descriptor apply to it.

  2. Apply rule to el to the specified value stage, with the following changes:

  3. Determine the computed value of all custom properties and the result "property" on el, as defined in CSS Properties and Values API 1 § 2.4 Computed Value-Time Behavior, with changes from the previous step, and the following:

    • Aside from references to custom properties (which use the values on el as normal) and numbers/percentages (which are left unresolved in custom properties, as normal), all values which would normally refer to the element being styled instead refer to calling context’s root element.

      Note: For example, attr() in a property, or @container queries in the rule.

  4. Return el’s styles.

    Note: Only custom properties and the result descriptor will be used from these styles.

4. Execution Model of Custom Functions

Like the rest of CSS, custom functions adhere to a declarative model.

The local variable descriptors and result descriptor can appear in any order, and may be provided multiple times. If this happens, then declarations appearing later win over earlier ones.

@function --mypi() {
  result: 3;
  result: 3.14;
}
The value of the result descriptor of --mypi is 3.14.
@function --circle-area(--r) {
  result: calc(pi * var(--r2));
  --r2: var(--r) * var(--r);
}
Local variable descriptors may appear before or after they are referenced.

4.1. Conditional Rules

A conditional group rule that appears within a @function becomes a nested group rule, with the additional restriction that only descriptors allowed within @function are allowed within the nested group rule.

Conditional group rules within @function are processed as normal, acting as if the contents of the rule were present at the conditional group rule’s location when the condition is true, or acting as if nothing exists at that location otherwise.

Tests
@function --suitable-font-size() {
  result: 16px;
  @media (width > 1000px) {
    result: 20px;
  }
}

The value of the result descriptor is 20px if the media query’s condition is true, and 16px otherwise.

Note that due to the execution model, "early return" is not possible within a @function:
@function --suitable-font-size() {
  @media (width > 1000px) {
    result: 20px;
  }
  result: 16px;
}

The value of the result descriptor is always 16px in the above example.

Local variables are also valid within conditional rules:
@function --suitable-font-size() {
  --size: 16px;
  @media (width > 1000px) {
    --size: 20px;
  }
  result: var(--size);
}

5. Defining Mixins

A mixin is in many ways similar to a custom function, but rather than extending/upgrading custom properties, mixins extend/upgrade nested style rules, making them reusable and customizable with arguments.

Tests
For example, the following code sets up a mixin applying all the properties you need for a "gradient text" effect, including guarding it with supports queries:
@mixin --gradient-text(
  --from <color>: mediumvioletred,
  --to <color>: teal,
  --angle: to bottom right,
) {
  @private {
    --gradient: linear-gradient(var(--angle), var(--from), var(--to));
  }
  color: var(--from, var(--to));

  @supports (background-clip: text) or (-webkit-background-clip: text) {
    background: var(--gradient, var(--from));
    color: transparent;
    -webkit-background-clip: text;
    background-clip: text;
  }
}

h1 {
  @apply --gradient-text(pink, powderblue);
}

Note that this example also uses a private property --gradient, which is accessible inside the mixin’s rule to aid in readability, but won’t pollute the element’s actual styles.

This is roughly equivalent to writing a nested style rule literally into the `h1` styles:

h1 {
  --from: pink;
  --to: powderblue;
  --angle: to bottom right;
  color: var(--from, var(--to));
  --gradient: linear-gradient(var(--angle), var(--from), var(--to));

  @supports (background-clip: text) or (-webkit-background-clip: text) {
    background: var(--gradient, var(--from));
    color: transparent;
    -webkit-background-clip: text;
    background-clip: text;
  }
}

(except that none of those custom properties actually show up in the element’s styles)

The entire @mixin feature is experimental and under active development, and is much less stable than @function. Expect things to change frequently for now.

5.1. The @mixin rule

The @mixin rule defines a mixin, and consists of a name, a list of mixin parameters, and a mixin body. (Identical to @function, save that it lacks a return type, and that the parameter list and enclosing parentheses are optional.)

<@mixin> = @mixin [<function-token> <function-parameter>#? ) | <ident-token> ]
{
  <block-contents>
}
Tests

The mixin body is parsed as if it were a nested declarations rule.

A @mixin rule cannot be a nested group rule; it is invalid within the body of a style rule.

5.1.1. The Mixin Prelude

The <function-token> or <ident-token> production must start with two dashes (U+002D HYPHEN-MINUS), similar to <dashed-ident>, or else the definition is invalid.

The name of the resulting mixin is given by the name of the <function-token> or <ident-token>. The optional mixin parameters are given by the <function-parameter> values (defaulting to an empty set).

If an <ident-token> is used, it is equivalent to a <function-token> followed by an empty parameter list.

The name of a @mixin rule is a tree-scoped name. If more than one @mixin exists for a given name, then the rule in the stronger cascade layer wins, and rules defined later win within the same layer.

If the mixin parameters contain the same <custom-property-name> more than once, then the @mixin rule is invalid.

If a mixin parameter has both a default value and a parameter type then the default value must parse successfully according to that parameter type’s syntax. Otherwise, the @mixin rule is invalid.

5.1.2. The Mixin Body

The body of a mixin is parsed as a nested declarations rule; it accepts properties, conditional group rules, and nested style rules, just like an ordinary style rule would. In particular, further mixins can be invoked.

In addition to everything normally valid in a style rule, the mixin body can contain one or more @contents rules, allowing a passed-in contents block to be substituted in.

Tests

5.2. Arguments and Local Variables

A mixin’s parameters define private properties in the mixin body, set to the value passed for the argument. As usual for private properties, these are accessible using var(), and shadow any same-named custom properties that might otherwise exist on the element. See § 5.5 Evaluating Mixins for details on this behavior.

For example, the following mixin use:
@mixin --shadowed-values(--color2: green, --color3: green) {
  @private { --color3: blue; }
  background: linear-gradient(var(--color1), var(--color2), var(--color3));
}
p {
  --color1: red;
  --color2: red;
  --color3: red;
  @apply --shadowed-values();
}

will produce a linear-gradient(red, green, blue) value, taking --color-1 from the outside (since nothing overrides it), --color2 from the mixin parameter (overriding the value from the element), and --color3 from the private property in the mixin body (overriding both the value from the element and the mixin parameter of that name).

Because they’re built on private properties, mixin parameters share the same potentially confusing weakness: all mentions of the private property name in the mixin body are rewritten to a unique private name, but the value is only visible to the applying element and its descendants. Using a nested rule with a selector that selects non-descendants, like & + * which grabs the element’s following sibling, can style elements that won’t actually see the passed argument value. For example, in the following code:
@mixin --only-descs(--arg) {
  color: var(--arg);
  & + * { color: var(--arg, red); }
}
.foo {
  @apply --only-descs(green);
}

The .foo element will get color: green, from the passed argument value, but its following sibling will get color: red, as it doesn’t see the private property value and instead triggers fallback.

Make sure, if you’re styling non-descendants in a mixin, not to rely on any mixin parameters in those styles.

Note: Even if the same mixin is applied on a parent and a child, each application of the mixin creates a unique set of styles with their own private properties, so the child’s sibling won’t see the parent’s --arg private property.

5.3. The @contents Rule

In addition to accepting arguments passed by the <dashed-function> in the @apply rule, a mixin can accept a contents block. Any mixin can be passed a contents block, by giving the @apply rule invoking the mixin a block.

This allows the invoker of the mixin to pass an entire style block, which the mixin can then substitute into itself. This is useful, for example, if the mixin handles some common conditions for the author, and substitutes the contents block into a predefined @media or @container rule.

The syntax of a @contents at-rule is:

<@contents> = @contents [ { <declaration-list> } ]?
Tests

That is, it is either an empty statement ended immediately by a semicolon, or a fallback block treated as a nested declarations rule. The empty statement form behaves identically to passing an empty block.

Outside of a mixin body, the @contents rule is invalid and ignored.

Note: It’s valid for an @apply rule to pass a contents block, but the mixin not use it.

For example, the following mixins abstracts the cases that the page would consider to be appropriate for a "single column" layout, allowing the rest of the page to handle the case without worrying about the details, so the conditions can be adjusted in the future if necessary:
@mixin --one-column() {
  @media (width <= 800px) {
    @contents;
  }
}
@mixin --two-column() {
  @media (width > 800px) {
    @contents;
  }
}
body {
  @apply --one-column {
    display: flex;
    flex-flow: column;
  }
  @apply --two-column {
    display: grid;
    grid-template-columns: 60px 60px;
  }
}

5.4. The @apply Rule

The @apply rule applies a mixin, causing it to substitute into the rule in place of the @apply rule itself.

Its grammar is:

<@apply> = @apply [ <dashed-ident> | <dashed-function> ] [ { <declaration-list> } ]?;
Tests
For example, a mixin can be applied in any of these ways:
.foo {
  @apply --one;
  /* Invokes the --one mixin, with no arguments or contents. */

  @apply --two(blue);
  /* Invokes --two with one argument, and no contents. */

  @apply --three {color: red;}
  /* Invokes --three with no arguments, but with contents. */

  @apply --four(blue) {color: red;}
  /* Invokes --four with both an argument and contents. */
}

The @apply rule is only valid in the body of a style rule or nested group rule; using it in any other context causes it to be invalid and ignored.

@apply rules are processed before any styles are applied, as they effectively modify the stylesheet itself. (Similar, in effect, to how conditional group rules adjust which properties and rules are active in a stylesheet before styles are applied.)

The @apply rule applies the mixin named by the <dashed-ident> or the <dashed-function>’s name. If no such mixin exists, the @apply does nothing.

If passed a <dashed-function>, the arguments passed to the <dashed-function> are mapped to the mixin’s arguments; if more arguments are passed than the length of the mixin’s argument list, the @apply application does nothing. (Passing too few arguments is fine; the missing arguments take their default values instead.) A <dashed-ident> passes no arguments. (That is, @apply --foo; is identical to @apply --foo();.)

If the @apply rule has a <declaration-list> block, that block is passed as the mixin’s contents block.

Applying a mixin without arguments, or with an empty argument list, is identical. That is, these two invocations do exactly the same thing:
.foo {
  @apply --no-args;
}
.bar {
  @apply --no-args();
}

Passing a contents block is not the same; omitting the block entirely triggers @contents fallback, while passing an empty block will substitute the empty block:

@mixin --just-contents() {
  @contents { color: red; }
  /* `color: red` is the fallback content */
}

.foo {
  @apply --just-contents;
  /* fallback, substitutes with `color: red;` */
}
.bar {
  @apply --just-contents {};
  /* substitutes with nothing at all */
}

5.5. Evaluating Mixins

TODO rewrite to use the new @private concepts.

At a high level, mixins are applied by substituting their contents at the location they’re @apply'd.

Unfortunately, the exact mechanics of mixin substitution are somewhat more complicated, to ensure that local references, variables, and other concepts work in a "natural" way.

When a mixin is applied, the @apply rule referencing it is replaced with the mixin result, with each property value in the mixin result having its value replaced by an anonymous custom function with the following properties:

Additionally, the arguments passed to the mixin are evaluated and stored on the @apply'd element, to ensure they obtain the "expected" value from that element, rather than unexpectedly evaluating in a descendant element’s context:

For example, given the following mixin:
@mixin --same-size(--size <length>) {
  @result {
    &, & > * {
      width: calc(10 * var(--size));
    }
  }
}
.parent {
  font-size: 10px;
  @apply --same-size(1em);
}
.parent > .child {
  font-size: 20px;
}

This will desugar into approximately:

@property --magic-arg1 {
  syntax: "<length>";
  inherits: true;
  /* initial-value: don't worry about it; */
}
@function --anonfunc1(--arg1) {
  result: calc(10 * var(--arg1));
}
@function --anonfunc2(--arg1) {
  result: calc(10 * var(--arg1));
}
.parent {
  font-size: 10px;
  --magic-arg1: 1em; /* resolves based on font-size here */
  width: --anonfunc1(var(--magic-arg1));
}
.parent > .child {
  font-size: 20px;
  width: --anonfunc2(var(--magic-arg1));
  /* --magic-arg1 is 10px, since it was resolved on the parent */
}

Note: In practice, this anonymous custom function can usually be completely hypothetical, and a direct substitution used instead. It’s required only to make variables and other element-relative values resolve correctly.

When mixins are nested (one invoked via @apply inside the @result of another), the desugaring nests as well, in the obvious way. For example:
div {
  @apply --colorized-squish(tomato);
}

/* "wraps" an element in colored arrows */
@mixin --squish(--left-color <color>,
                --right-color <color>: var(--left-color)) {
  @result {
    &::before {
      content: "🡆";
      background-color: var(--left-color);
    }
    &::after {
      content: "🡄";
      background-color: var(--right-color);
    }
  }
}

/* colors the element, and auto-generates a border color
   and the "squish" colors from it */
@mixin --colorized-squish(--color <color>) {
  @result {
    background-color: var(--color);
    border: 2px solid oklch(from var(--color) calc(l - 0.1) c h);
    @apply --squish(oklch(from var(--color) calc(l - 0.3) c h),
                    oklch(from var(--color) calc(l - 0.2) c h));
  }
}

This desugars in two steps, inside-out. First, the --squish() is unfolded into the --colorized-squish() mixin:

div {
  @apply --colorized-squish(tomato);
}

@mixin --colorized-squish(--color <color>) {
  /* Lift the `@apply squish();` arguments out into local vars */
  --s-arg1: oklch(from var(--color) calc(l - 0.3) c h);
  --s-arg2: oklch(from var(--color) calc(l - 0.2) c h);
  @result {
    background-color: var(--color);
    border: 2px solid oklch(from var(--color) calc(l - 0.1) c h);
    /* Replace the `@apply --squish();` with its @result,
       transformed to wrap the property values in
       anonymous functions. */
    &::before {
      content: --s1(var(--s-arg1), var(--s-arg2));
      background-color: --s2(var(--s-arg1), var(--s-arg2));
    }
    &::after {
      content: --s3(var(--s-arg1), var(--s-arg2));
      background-color: --s4(var(--s-arg1), var(--s-arg2));
    }
  }
}
@function --s1(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: "🡆";
}
@function --s2(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: var(--left-color);
}
@function --s3(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: "🡄";
}
@function --s4(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: var(--right-color);
}

Then the --colorized-squish() mixin is unfolded into the div rule:

div {
  /* Lift the `@apply --colorized-squish();` argument out into a var */
  --cs-arg1: tomato;
  /* Replace the `@apply --colorized-squish();` with (part of)
     its @result, again with values wrapped in anonymous functions. */
  background-color: --cs1(var(--cs-arg1));
  border: --cs2(var(--cs-arg1));
  &::before {
    /* Note that --cs-arg1 is inherited here from div */
    content: --cs3(var(--cs-arg1));
    background-color: --cs4(var(--cs-arg1));
  }
  &::after {
    content: --cs5(var(--cs-arg1));
    background-color: --cs6(var(--cs-arg1));
  }
}
@function --cs1(--color <color>) {
  result: var(--color);
}
@function --cs2(--color <color>) {
  result: 2px solid oklch(from var(--color) calc(l - 0.1) c h);
}

/* These four generated functions all look identical, they
   just call into the correct generated function from
   --squish()'s unfolding. */
@function --cs3(--color <color>) {
  /* The --colorized-squish() local vars (created by the
    first desugaring) are turned into function local vars. */
  --s-arg1: oklch(from var(--color) calc(l - 0.3) c h);
  --s-arg2: oklch(from var(--color) calc(l - 0.2) c h);
  result: --s1(var(--s-arg1), var(--s-arg2));
}
@function --cs4(--color <color>) {
  --s-arg1: oklch(from var(--color) calc(l - 0.3) c h);
  --s-arg2: oklch(from var(--color) calc(l - 0.2) c h);
  result: --s2(var(--s-arg1), var(--s-arg2));
}
@function --cs5(--color <color>) {
  --s-arg1: oklch(from var(--color) calc(l - 0.3) c h);
  --s-arg2: oklch(from var(--color) calc(l - 0.2) c h);
  result: --s3(var(--s-arg1), var(--s-arg2));
}
@function --cs6(--color <color>) {
  --s-arg1: oklch(from var(--color) calc(l - 0.3) c h);
  --s-arg2: oklch(from var(--color) calc(l - 0.2) c h);
  result: --s4(var(--s-arg1), var(--s-arg2));
}

/* These are copied from the previous desugaring */
@function --s1(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: "🡆";
}
@function --s2(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: var(--left-color);
}
@function --s3(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: "🡄";
}
@function --s4(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: var(--right-color);
}

Note that all the custom property and function names are given short, somewhat meaningful names here, for readability, but actually are hygienically renamed and guaranteed to be unreferencable by author code. Only the user agent ever sees or can use them, to do this desugaring.

Desugaring outside-in

Equivalently, the mixins can be desugared outside-in, for the same result:

/* Initial Code, same as main example */
div {
  @apply --colorized-squish(tomato);
}

/* "wraps" an element in colored arrows */
@mixin --squish(--left-color <color>,
                --right-color <color>: var(--left-color)) {
  @result {
    &::before {
      content: "🡆";
      background-color: var(--left-color);
    }
    &::after {
      content: "🡄";
      background-color: var(--right-color);
    }
  }
}

/* colors the element, and auto-generates a border color
   and the "squish" colors from it */
@mixin --colorized-squish(--color <color>) {
  @result {
    background-color: var(--color);
    border: 2px solid oklch(from var(--color) calc(l - 0.1) c h);
    @apply --squish(oklch(from var(--color) calc(l - 0.3) c h),
                    oklch(from var(--color) calc(l - 0.2) c h));
  }
}


/* First desugar, unfolding --colorized-squish()*/

div {
  /* Pull out the --colorized-squish() argument */
  --cs-arg1: tomato;
  /* Swap the @apply with the --colorized-squish() @result,
     with values wrapped in anonymous functions. */
  background-color: --cs1(var(--cs-arg1));
  border: --cs2(var(--cs-arg1));
  /* The equivalent transform here is wrapping the
     @apply args in anonymous functions. */
  @apply --squish(--cs3(--cs-arg1),
                --cs4(--cs-arg1));
}
@function --cs1(--color <color>) {
  result: var(--color);
}
@function --cs2(--color <color>) {
  result: 2px solid oklch(from var(--color) calc(l - 0.1) c h);
}
@function --cs3(--color <color>) {
  result: oklch(from var(--color) calc(l - 0.3) c h);
}
@function --cs4(--color <color>) {
  result: oklch(from var(--color) calc(l - 0.2) c h);
}
@mixin --squish(--left-color <color>,
                --right-color <color>: var(--left-color)) {
  @result {
    &::before {
      content: "🡆";
      background-color: var(--left-color);
    }
    &::after {
      content: "🡄";
      background-color: var(--right-color);
    }
  }
}


/* Second desugar, unfolding --squish() */
div {
  --cs-arg1: tomato;
  background-color: --cs1(var(--cs-arg1));
  border: --cs1(var(--cs-arg1));
  /* Pull out the --squish() args now. */
  --s-arg1: --cs3(--cs-arg1);
  --s-arg2: --cs4(--cs-arg1);
  /* And swap the @apply with the --squish() @result,
     with values wrapped in anonymous functions. */
  &::before {
    content: --s1(var(--s-arg1), var(--s-arg2));
    background-color: --s2(var(--s-arg1), var(--s-arg2));
  }
  &::after {
    content: --s3(var(--s-arg1), var(--s-arg2));
    background-color: --s4(var(--s-arg1), var(--s-arg2));
  }
}
@function --cs1(--color <color>) {
  result: var(--color);
}
@function --cs2(--color <color>) {
  result: 2px solid oklch(from var(--color) calc(l - 0.1) c h);
}
@function --cs3(--color <color>) {
  result: oklch(from var(--color) calc(l - 0.3) c h);
}
@function --cs4(--color <color>) {
  result: oklch(from var(--color) calc(l - 0.2) c h);
}
@function --s1(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: "🡆";
}
@function --s2(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: var(--left-color);
}
@function --s3(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: "🡄";
}
@function --s4(--left-color <color>,
               --right-color <color>: var(--left-color)) {
  result: var(--right-color);
}

The desugaring is slightly different in details, like the --s-arg1/2 custom properties are stored on div rather than inside of the functions, but because they’re unobservable and hygienically renamed, the behavior is identical.

6. Private Custom Properties: the @private rule

"Temporary variables" are a common tool in programming languages, letting one give a name to an intermediate result, then use that name in another value to make it easier to read. One can use custom properties for this purpose, but they’re automatically exposed to descendant elements on the page, and might clash with existing custom properties used on the element, requiring some degree of coordination between otherwise-independent usages. Mixins make this problem even worse, since its mixin parameters are exposed as custom properties for the applying element and its descendants to use.

To address these use-cases, the @private rule can be nested inside of any style rule, and defines private properties: a special kind of custom property which is only usable inside the same style rule (and any nested style rules).

<@private> = @private { <declaration-rule-list> }

The @private rule is only valid nested as a child of a style rule or a nested group rule. In any other context, it is invalid.

A @private rule can only contain custom properties and nested group rules (in which case it passes this same restriction to their contents). Any other contents are invalid and will be ignored, but do not invalidate the @private rule itself.

When a style rule contains any @private rules (including those nested in nested group rules), the set of custom properties they contain define the style rule’s private property names, a set of custom property names. Within the style rule (and any nested rules), any custom property with that name is actually a private property, and any reference to a custom property of that name (for example, with a var()) is a reference to the private property.

A private property is a special variant of a custom property:

Distinct private properties with the same name are, for all intents and purposes, separate properties. They can exist together on a single element at the same time, carry their own distinct values, etc.

Note: Because private properties are different from same-named custom properties, custom property registrations do not normally apply to them. (They apply to the non-private custom property, instead.) In some cases, such as mixin parameters, CSS can still give them a custom property registration.

Applying a mixin with @apply effectively substitutes the mixin in-place, so a mixin sees any private properties visible to the style rule it’s substituted into (if it doesn’t override them with its own mixin parameters or private properties).

What about custom functions? If they refer to a custom property not defined in the function, should they see an outer private property? I’m inclined to say yes; extracting some styles to a custom function should, generally, act identically to inlining them.

For example, in the following styles, every style rule is referring to the same custom property:
section {
  --size: 2px;
  &, & > *, & > * > * {
    border-width: var(--size);
  }
}
section > h1 {
  --size: 4px;
}
section > h1 > small {
  --size: 3px;
}

As a result, the elements will have three different border-widths.

If we changed that to use a private property in the first rule, however:

section {
  @private { --size: 2px; }
  &, & > *, & > * > * {
    border-width: var(--size);
  }
}
section > h1 {
  --size: 4px;
}
section > h1 > small {
  --size: 3px;
}

Within the first section rule and its nested rules, --size is a private property, and the var(--size) refers to the private property. In the later, non-nested rules, --size is a normal custom property, independent of the private property from the first rule, so setting it to other values won’t affect the first usage. Instead, it’s roughly equivalent to:

section {
  --f7bd60b7: 2px;
  border-width: var(--f7bd60b7);
}
section > h1 {
  --size: 4px;
  border-width: var(--f7bd60b7);
}
section > h1 > small {
  --size: 3px;
  border-width: var(--f7bd60b7);
}

All three elements end up with identical border-width values of 2px.

Note: While private properties ensure that elements and their descendants can’t accidentally have their variables overwritten by accidental collisions, they’re still evaluated as normal for properties, which can interact with other properties set in other style rules.

For example, in the following variant of the previous example:
section {
  font-size: 10px;
  @private { --size: .2em; }
  &, & > *, & > * > * {
    border-width: var(--size);
  }
}
section > h1 {
  font-size: 20px;
}
section > h1 > small {
  font-size: 15px;
}

The styles are roughly equivalent to:

section {
  font-size: 10px;
  border-width: .2em;
}
section > h1 {
  font-size: 20px;
  border-width: .2em;
}
section > h1 > small {
  font-size: 15px;
  border-width: .2em;
}

Which will give three different border-width values: 2px, 4px, and 3px.

Because private properties generally can’t be registered custom properties, if you did want the preceding example to resolve the em length against the section element, you can’t get that behavior by default.

This can be resolved with a custom function, and might be addressed more directly in CSS in the future:

@function --as-length(--x <length>) { result: var(--x); }
section {
  font-size: 10px;
  @private { --size: --as-length(.2em); }
  &, & > *, & > * > * {
    border-width: var(--size);
  }
}
section > h1 {
  font-size: 20px;
}
section > h1 > small {
  font-size: 15px;
}

Here, because the custom function argument has type <length>, it computes the .2em value against the element’s font-size, returning 2px. This then inherits as the px value to its descendants, and is unaffected by changes in font-size.

Many of the example above work identically with mixins, since mixin parameters define private properties. Notably, whether a mixin parameter is typed or untyped can change its visible behavior in the same ways as the previous examples illustrated. Given the following two nearly-identical mixins:
@mixin --untyped(--size) {
  &, & > *, & > * > * {
    border-width: var(--size);
  }
}
@mixin --typed(--size <length>) {
  &, & > *, & > * > * {
    border-width: var(--size);
  }
}

Then, in the following examples we get various behaviors:

section {
  @apply --untyped(2px);
}
section > h1 {
  --size: 4px;
}
section > h1 > small {
  --size: 3px;
}

In this example, the --size custom properties are distinct from the --size private property defined by the mixin parameter, so all three elements share a 2px border-width.

section {
  font-size: 10px;
  @apply --untyped(.2em);
}
section > h1 {
  font-size: 20px;
}
section > h1 > small {
  font-size: 15px;
}

In this example, the .2em is left unresolved in the mixin parameter, so every element gets its own border-width: .2em value, resolving to three different lengths based on each element’s font-size.

section {
  font-size: 10px;
  @apply --typed(.2em);
}
section > h1 {
  font-size: 20px;
}
section > h1 > small {
  font-size: 15px;
}

In this example, because the mixin parameter is typed as <length>, it resolves the .2em to 2px on the applying element, and all three elements share a 2px border-width.

7. CSSOM

TODO Supply the OM for @mixin/etc.

Tests

7.1. The CSSFunctionRule Interface

The CSSFunctionRule interface represents a @function rule.

[Exposed=Window]
interface CSSFunctionRule : CSSGroupingRule {
  readonly attribute CSSOMString name;
  sequence<FunctionParameter> getParameters();
  readonly attribute CSSOMString returnType;
};
name, of type CSSOMString, readonly
The name of the custom function.
returnType, of type CSSOMString, readonly
The return type of the custom function, represented as a syntax string. If the custom function has no return type, returns "*".
dictionary FunctionParameter {
  required CSSOMString name;
  required CSSOMString type;
  CSSOMString defaultValue;
};
name
The name of the function parameter.
type
The type of the function parameter, represented as a syntax string, or "*" if the parameter has no type.
defaultValue
The default value of the function parameter, or `null` if the argument does not have a default.

While declarations may be specified directly within a @function rule, they are not represented as such in the CSSOM. Instead, consecutive segments of declarations appear as if wrapped in CSSFunctionDeclarations rules.

Note: This also applies to the "leading" declarations in the @function rule, i.e those that do not follow another nested rule.

@function --bar() {
  --x: 42;
  result: var(--y);
  @media (width > 1000px) {
    /* ... */
  }
  --y: var(--x);
}

The above will appear in the CSSOM as:

@function --bar() {
  /* CSSFunctionDeclarations { */
    --x: 42;
    result: var(--y);
  /* } */
  @media (width > 1000px) {
    /* ... */
  }
  /* CSSFunctionDeclarations { */
    --y: var(--x);
  /* } */
}
To serialize a CSSFunctionRule, return the concatenation of the following:
  1. The string "@function" followed by a single SPACE (U+0020).

  2. The result of performing serialize an identifier on the name of the custom function, followed by a single LEFT PARENTHESIS (U+0028).

  3. The result of serialize a function parameter on each of the custom function’s parameters, all joined by ", " (COMMA U+002C, followed by a single SPACE U+0020).

  4. A single RIGHT PARENTHESIS (U+0029).

  5. If the custom function has return type, and that return type is not the universal syntax definition ("*"):

    • A single SPACE (U+0020), followed by the string "returns", followed by a single SPACE (U+0020).

    • The result of performing serialize a CSS type on that type.

  6. A single SPACE (U+0020), followed by a LEFT CURLY BRACKET (U+007B).

  7. The result of performing serialize a CSS rule on each rule in cssRules, filtering out empty strings, each preceded by a single SPACE (U+0020).

    Note: Serialize a CSS rule can return an empty string when serializing an empty CSSFunctionDeclarations rule.

  8. A single SPACE (U+0020), followed by a single RIGHT CURLY BRACKET (U+007D).

To serialize a function parameter, return the concatenation of the following:
  1. The result of performing serialize an identifier on the name of the function parameter.

  2. If the function parameter has a type, and that type is not the universal syntax definition:

  3. If the function parameter has a default value:

    • A single COLON (U+003A), followed by a single SPACE (U+0020), followed by the result of performing serialize a CSS value on that value.

To serialize a CSS type, return the concatenation of the following:
  1. If the <css-type> consists of a single <syntax-component>, return the corresponding syntax string.

  2. Otherwise, return the concatenation of the following:

    • The string "type(", i.e. "type" followed by a single LEFT PARENTHESIS (U+0028).

    • The corresponding syntax string.

    • The string ")", i.e. a single RIGHT PARENTHESIS (U+0029).

7.2. The CSSFunctionDeclarations Interface

The CSSFunctionDeclarations interface represents a run of consecutive declarations within a @function rule.

[Exposed=Window]
interface CSSFunctionDescriptors : CSSStyleDeclaration {
  attribute [LegacyNullToEmptyString] CSSOMString result;
};

[Exposed=Window]
interface CSSFunctionDeclarations : CSSRule {
  [SameObject, PutForwards=cssText] readonly attribute CSSFunctionDescriptors style;
};
The style attribute must return a CSSFunctionDescriptors object for the rule, with the following properties:
computed flag

Unset

readonly flag

Unset

declarations

The declared declarations in the rule, in specified order. This includes any local variables.

parent CSS rule

this

owner node

Null

The CSSFunctionDeclarations rule, like CSSNestedDeclarations, serializes as if its declaration block had been serialized directly.

7.3. The CSSMixinRule Interface

The CSSMixinRule interface represents a @mixin rule.

[Exposed=Window]
interface CSSMixinRule : CSSGroupingRule {
  readonly attribute CSSOMString name;
  sequence<FunctionParameter> getParameters();
  readonly attribute boolean contents;
};
name, of type CSSOMString, readonly
The result of serialize an identifier on the mixin’s name.
getParameters()
Returns function parameters associated with the mixin, excluding any '@contents' parameter.
contents, of type boolean, readonly
True if the mixin accepts a contents block, and false otherwise.

While declarations may be specified directly within a @mixin rule, they are not represented as such in the CSSOM. Instead, consecutive segments of declarations appear as if wrapped in CSSNestedDeclarations rules.

This is similar to how segments of bare declarations within '@function' are wrapped, except with CSSNestedDeclarations as the wrapper rather than CSSFunctionDeclarations.
To serialize a CSSMixinRule, return the concatenation of the following:
  1. The string "@mixin" followed by a single SPACE (U+0020).

  2. The result of performing serialize an identifier on the name of the mixin, followed by a single LEFT PARENTHESIS (U+0028).

  3. The result of serialize a function parameter on each of the mixin’s parameters, all joined by ", " (COMMA U+002C, followed by a single SPACE U+0020).

  4. A single RIGHT PARENTHESIS (U+0029).

  5. A single LEFT CURLY BRACKET (U+007B), followed by a SPACE (U+0020).

  6. The result of performing serialize a CSS rule on each rule in cssRules, filtering out empty strings, all joined by a single SPACE (U+0020).

  7. A single SPACE (U+0020), followed by a single RIGHT CURLY BRACKET (U+007D).

7.4. The CSSApplyBlockRule Interface

The CSSApplyBlockRule interface represents an @apply rule with a contents block.

[Exposed=Window]
interface CSSApplyBlockRule : CSSGroupingRule {
  readonly attribute CSSOMString name;
  sequence<CSSOMString> getArguments();
};
name, of type CSSOMString, readonly
If the @apply rule has an associated <dashed-ident>, the result of serialize an identifier on that ident. Otherwise, the result of serialize an identifier on the name of the associated <dashed-function>.
getArguments()
Returns a sequence of arguments as strings, each item serialized as if it had been the specified value of a custom property.

The child CSS rules of a CSSApplyBlockRule represent the contents block of the @apply rule.

As for CSSMixinRule, consecutive segments of declarations specified directly within the contents block of an @apply rule are represented as CSSNestedDeclarations.

To serialize a CSSApplyBlockRule, return the concatenation of the following:
  1. The string "@apply" followed by a single SPACE (U+0020).

  2. The result of performing serialize an @apply prelude on the @apply rule.

  3. A single SPACE (U+0020).

  4. A single LEFT CURLY BRACKET (U+007B), followed by a SPACE (U+0020).

  5. The result of performing serialize a CSS rule on each rule in cssRules, filtering out empty strings, all joined by a single SPACE (U+0020).

  6. A single SPACE (U+0020), followed by a RIGHT CURLY BRACKET (U+007D).

To serialize an @apply prelude, given an @apply rule, return the concatenation of the following:
  1. The name of the @apply rule.

  2. If the @apply rule has at least one argument, the concatenation of:

    • A single LEFT PARENTHESIS (U+0028).

    • The result of performing serialize a function argument on each argument, all joined by ", ".

    • A single RIGHT PARENTHESIS (U+0029).

To serialize a function argument, given a <declaration-value> argument:
  1. Serialize argument as if it had been the specified value of a custom property, and let serialized argument be the result.

    This means that an argument serializes exactly as written, except with leading and trailing whitespace removed.
  2. If serialized argument does not contain any top-level <comma-token>s, nor any top-level <{-token>s, return serialized argument.

  3. Otherwise, return the concatenation of the following:

    • A single LEFT CURLY BRACKET (U+007B), followed by a SPACE (U+0020).

    • The value of serialized argument.

    • A single SPACE (U+0020), followed by a RIGHT CURLY BRACKET (U+007D).

7.5. The CSSApplyStatementRule Interface

The CSSApplyStatementRule interface represents an @apply rule without a contents block.

[Exposed=Window]
interface CSSApplyStatementRule : CSSRule { 
  readonly attribute CSSOMString name;
  sequence<CSSOMString> getArguments();
};
name, of type CSSOMString, readonly
The same as CSSApplyBlockRule.name.
getArguments()
The same as CSSApplyBlockRule.getArguments().
To serialize a CSSApplyStatementRule, return the concatenation of the following:
  1. The string "@apply" followed by a single SPACE (U+0020).

  2. The result of performing serialize an @apply prelude on the @apply rule.

  3. A single SEMICOLON (U+003B).

7.6. The CSSContentsBlockRule Interface

The CSSContentsBlockRule interface represents a @contents rule with a fallback block.

[Exposed=Window]
interface CSSContentsBlockRule : CSSGroupingRule { };

The child CSS rules of a CSSContentsBlockRule represent the fallback block of the @contents rule.

As for CSSMixinRule and CSSApplyBlockRule, consecutive segments of declarations specified directly within the fallback block of a @contents rule are represented as CSSNestedDeclarations.

To serialize a CSSContentsBlockRule, return the concatenation of the following:
  1. The string "@contents" followed by a single SPACE (U+0020).

  2. A single LEFT CURLY BRACKET (U+007B), followed by a SPACE (U+0020).

  3. The result of performing serialize a CSS rule on each rule in cssRules, filtering out empty strings, all joined by a single SPACE (U+0020).

  4. A single SPACE (U+0020), followed by a RIGHT CURLY BRACKET (U+007D).

7.7. The CSSContentsStatementRule Interface

The CSSContentsStatementRule interface represents a @contents rule without a fallback block.

[Exposed=Window]
interface CSSContentsStatementRule : CSSRule { };
To serialize a CSSContentsStatementRule, return the string "@contents", followed by a single SEMICOLON (U+003B).

8. Privacy Considerations

The constructs defined by this specification are defined and used entirely within CSS; they expose no new information.

9. Security Considerations

No issues have been opened against this specification.

10. Changes

Changes since the First Public Working Draft of 15 May 2025:

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Advisements are normative sections styled to evoke special attention and are set apart from other normative text with <strong class="advisement">, like this: UAs MUST provide an accessible alternative.

Tests

Tests relating to the content of this specification may be documented in “Tests” blocks like this one. Any such block is non-normative.


Conformance classes

Conformance to this specification is defined for three conformance classes:

style sheet
A CSS style sheet.
renderer
A UA that interprets the semantics of a style sheet and renders documents that use them.
authoring tool
A UA that writes a style sheet.

A style sheet is conformant to this specification if all of its statements that use syntax defined in this module are valid according to the generic CSS grammar and the individual grammars of each feature defined in this module.

A renderer is conformant to this specification if, in addition to interpreting the style sheet as defined by the appropriate specifications, it supports all the features defined by this specification by parsing them correctly and rendering the document accordingly. However, the inability of a UA to correctly render a document due to limitations of the device does not make the UA non-conformant. (For example, a UA is not required to render color on a monochrome monitor.)

An authoring tool is conformant to this specification if it writes style sheets that are syntactically correct according to the generic CSS grammar and the individual grammars of each feature in this module, and meet all other conformance requirements of style sheets as described in this module.

Partial implementations

So that authors can exploit the forward-compatible parsing rules to assign fallback values, CSS renderers must treat as invalid (and ignore as appropriate) any at-rules, properties, property values, keywords, and other syntactic constructs for which they have no usable level of support. In particular, user agents must not selectively ignore unsupported component values and honor supported values in a single multi-value property declaration: if any value is considered invalid (as unsupported values must be), CSS requires that the entire declaration be ignored.

Implementations of Unstable and Proprietary Features

To avoid clashes with future stable CSS features, the CSSWG recommends following best practices for the implementation of unstable features and proprietary extensions to CSS.

Non-experimental implementations

Once a specification reaches the Candidate Recommendation stage, non-experimental implementations are possible, and implementors should release an unprefixed implementation of any CR-level feature they can demonstrate to be correctly implemented according to spec.

To establish and maintain the interoperability of CSS across implementations, the CSS Working Group requests that non-experimental CSS renderers submit an implementation report (and, if necessary, the testcases used for that implementation report) to the W3C before releasing an unprefixed implementation of any CSS features. Testcases submitted to W3C are subject to review and correction by the CSS Working Group.

Further information on submitting testcases and implementation reports can be found from on the CSS Working Group’s website at http://www.w3.org/Style/CSS/Test/. Questions should be directed to the public-css-testsuite@w3.org mailing list.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSS-ANIMATIONS-1]
David Baron; et al. CSS Animations Level 1. URL: https://drafts.csswg.org/css-animations/
[CSS-CASCADE-5]
Elika Etemad; Miriam Suzanne; Tab Atkins Jr.. CSS Cascading and Inheritance Level 5. URL: https://drafts.csswg.org/css-cascade-5/
[CSS-CONDITIONAL-3]
Chris Lilley; David Baron; Elika Etemad. CSS Conditional Rules Module Level 3. URL: https://drafts.csswg.org/css-conditional-3/
[CSS-CONDITIONAL-5]
Chris Lilley; et al. CSS Conditional Rules Module Level 5. URL: https://drafts.csswg.org/css-conditional-5/
[CSS-NESTING-1]
Tab Atkins Jr.. CSS Nesting Module Level 1. URL: https://drafts.csswg.org/css-nesting/
[CSS-PROPERTIES-VALUES-API-1]
Tab Atkins Jr.; Alan Stearns; Greg Whitworth. CSS Properties and Values API Level 1. URL: https://drafts.css-houdini.org/css-properties-values-api-1/
[CSS-SHADOW-1]
CSS Shadow Module Level 1. Editor's Draft. URL: https://drafts.csswg.org/css-shadow-1/
[CSS-SYNTAX-3]
Tab Atkins Jr.; Simon Sapin. CSS Syntax Module Level 3. URL: https://drafts.csswg.org/css-syntax/
[CSS-VALUES-4]
Tab Atkins Jr.; Elika Etemad. CSS Values and Units Module Level 4. URL: https://drafts.csswg.org/css-values-4/
[CSS-VALUES-5]
Tab Atkins Jr.; Elika Etemad; Miriam Suzanne. CSS Values and Units Module Level 5. URL: https://drafts.csswg.org/css-values-5/
[CSS-VARIABLES-1]
Tab Atkins Jr.. CSS Custom Properties for Cascading Variables Module Level 1. URL: https://drafts.csswg.org/css-variables/
[CSS-VARIABLES-2]
CSS Custom Properties for Cascading Variables Module Level 2. Editor's Draft. URL: https://drafts.csswg.org/css-variables-2/
[CSSOM-1]
Daniel Glazman; Emilio Cobos Álvarez. CSS Object Model (CSSOM). URL: https://drafts.csswg.org/cssom/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[SELECTORS-4]
Elika Etemad; Tab Atkins Jr.. Selectors Level 4. URL: https://drafts.csswg.org/selectors/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Non-Normative References

[CSS-BORDERS-4]
Elika Etemad; et al. CSS Borders and Box Decorations Module Level 4. URL: https://drafts.csswg.org/css-borders-4/
[CSS-COLOR-4]
Tab Atkins Jr.; Chris Lilley; Lea Verou. CSS Color Module Level 4. URL: https://drafts.csswg.org/css-color-4/
[CSS-COLOR-5]
Chris Lilley; Una Kravets; Lea Verou. CSS Color Module Level 5. URL: https://drafts.csswg.org/css-color-5/
[CSS-FONTS-4]
Chris Lilley. CSS Fonts Module Level 4. URL: https://drafts.csswg.org/css-fonts-4/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/

Property Index

No properties defined.

@function Descriptors

Name Value Initial
result <declaration-value>? n/a (see prose)

IDL Index

[Exposed=Window]
interface CSSFunctionRule : CSSGroupingRule {
  readonly attribute CSSOMString name;
  sequence<FunctionParameter> getParameters();
  readonly attribute CSSOMString returnType;
};

dictionary FunctionParameter {
  required CSSOMString name;
  required CSSOMString type;
  CSSOMString defaultValue;
};

[Exposed=Window]
interface CSSFunctionDescriptors : CSSStyleDeclaration {
  attribute [LegacyNullToEmptyString] CSSOMString result;
};

[Exposed=Window]
interface CSSFunctionDeclarations : CSSRule {
  [SameObject, PutForwards=cssText] readonly attribute CSSFunctionDescriptors style;
};

[Exposed=Window]
interface CSSMixinRule : CSSGroupingRule {
  readonly attribute CSSOMString name;
  sequence<FunctionParameter> getParameters();
  readonly attribute boolean contents;
};

[Exposed=Window]
interface CSSApplyBlockRule : CSSGroupingRule {
  readonly attribute CSSOMString name;
  sequence<CSSOMString> getArguments();
};

[Exposed=Window]
interface CSSApplyStatementRule : CSSRule { 
  readonly attribute CSSOMString name;
  sequence<CSSOMString> getArguments();
};

[Exposed=Window]
interface CSSContentsBlockRule : CSSGroupingRule { };

[Exposed=Window]
interface CSSContentsStatementRule : CSSRule { };

Issues Index

The entire @mixin feature is experimental and under active development, and is much less stable than @function. Expect things to change frequently for now.
TODO rewrite to use the new @private concepts.
What about custom functions? If they refer to a custom property not defined in the function, should they see an outer private property? I’m inclined to say yes; extracting some styles to a custom function should, generally, act identically to inlining them.
TODO Supply the OM for @mixin/etc.