0Support

CSS @function playground

Build custom CSS functions, add or remove parameters with types and default values, combine multiple result descriptors with @media, and copy a complete example ready to test.

Read the guide

Ask AI

CSS @function playground

Response generated with openai/gpt-5.4-nano. AI can make mistakes. Always review the result.
Call--responsive-size(1.35rem)
HTML
<article class="specimen">
  <p class="specimen-eyebrow">CSS custom function</p>
  <h1>One formula, many results</h1>
  <p>The function result is applied directly to this example component.</p>
</article>
CSS
@function --responsive-size(--narrow <length>, --wide <length>: 2.65rem) returns <length> {
  result: var(--wide);

  @media (width < 500px) {
    result: var(--narrow);
  }
}

.specimen {
  padding: clamp(1.25rem, 6vw, 2.75rem);
  display: grid;
  gap: 0.75rem;
  background: #171a28e8;
  color: #f5f7ff;
  font-family: ui-monospace, "SFMono-Regular", Consolas, monospace;
  font-size: --responsive-size(1.35rem);
}

.specimen-eyebrow {
  margin: 0;
  color: #ff9a6f;
  font-size: 0.72rem;
  font-weight: 800;
  text-transform: uppercase;
}

.specimen h1 {
  margin: 0;
  font-size: 1em;
  font-weight: 800;
  line-height: 1.05;
  text-wrap: balance;
}

.specimen > p:last-child {
  margin: 0;
  font-family: ui-sans-serif, system-ui, sans-serif;
  font-size: 0.9rem;
}

Educational guide to @function

@function: reusable formulas with CSS values

A custom property stores a value; a CSS function accepts arguments and computes a value at the call site. Learn to define its signature, resolve result, and keep an alternative when support is missing.

Back to playground

Mental model

Defining a function does not apply styles: call it inside a property value.

  1. Signature and arguments

    --guide-space(--step <number>: 2) declares a parameter. --guide-space(1) passes an argument.

  2. Local resolution

    var(--step) reads the parameter; local values and conditions determine result.

  3. Property value

    The result replaces the call in padding. It must satisfy both returns and the padding grammar.

A value, not a block of stylesStatic diagram: arguments → computation → value. The function does not create elements or apply sets of declarations like a mixin.

Function and parameter names start with -- and are case-sensitive. Calls use parentheses, not var(--function-name). Define @function outside style rules and use it where you would accept a CSS value; it can take no parameters.

Parameters, types, and default values

The signature describes the inputs and returns constrains the output value.

Separate parameters with commas. Types are optional: omitting one accepts type(*). A default follows a colon and must match its type. Here --step is a <number>, but the calculation produces a <length>. Read parameters with var(); calls accept values, not parameter names.

A spacing scale with an optional argument

--guide-space(): default 2

--guide-space(1): argument 1

Native CSS function: compare spacing with the expected result.

Base fallback: 1rem padding; call syntax is unavailable.

With native support, the first box has 1.5rem of padding and the second 0.75rem. Without support, both keep 1rem. The dashed border marks the content so you can see the spacing.
A spacing scale with an optional argument
<div class="function-scene-signature">
  <div class="sample"><p>--guide-space(): default 2</p></div>
  <div class="sample compact"><p>--guide-space(1): argument 1</p></div>
  <p class="native">Native CSS function: compare spacing with the expected result.</p>
  <p class="fallback">Base fallback: 1rem padding; call syntax is unavailable.</p>
</div>
@function --guide-space(--step <number>: 2) returns <length> {
  result: calc(var(--step) * 0.75rem);
}

.function-scene-signature {
  display: grid;
  gap: 1rem;
  min-inline-size: 0;
  font: 1rem/1.5 system-ui, sans-serif;
  overflow-wrap: anywhere;

  .sample {
    padding: 1rem;
    border: 1px solid currentColor;

    p {
      margin: 0;
      border: 1px dashed currentColor;
    }
  }

  > p {
    margin: 0;
  }

  .native {
    display: none;
  }

  @supports (padding: --guide-space()) {
    .sample {
      padding: --guide-space();
    }

    .compact {
      padding: --guide-space(1);
    }

    .native {
      display: block;
    }

    .fallback {
      display: none;
    }
  }
}

Omitting the argument uses 2; passing 1 overrides it. An incompatible argument can also trigger the default; without a default, a missing or incompatible required argument invalidates the call. <length> does not accept percentages: use <length-percentage> if you need both. type(<number> | <percentage>) allows a union, but your formula must still handle both types.

Scope: parameters, locals, and the element

A function is evaluated in the calling context, unlike a value fixed on :root.

var() checks local variables first, then parameters, then custom properties available at the call site. A local variable shadows a parameter with the same name; a parameter shadows the element property. Locals are not published as properties in the DOM.

Same function, different unit per component

--unit: 0.5rem; --step parameter: 2

--unit: 1rem; --step parameter: 2

Native CSS function: compare spacing with the expected result.

Base fallback: 1rem padding; call syntax is unavailable.

With support, the --step parameter remains 2 even though the boxes have --step: 99. --unit does come from each box: padding is 1rem and 2rem. Without support, both use 1rem.
Same function, different unit per component
<div class="function-scene-scope">
  <div class="sample"><p>--unit: 0.5rem; --step parameter: 2</p></div>
  <div class="sample large"><p>--unit: 1rem; --step parameter: 2</p></div>
  <p class="native">Native CSS function: compare spacing with the expected result.</p>
  <p class="fallback">Base fallback: 1rem padding; call syntax is unavailable.</p>
</div>
@function --guide-local-space(--step <number>: 2) returns <length> {
  --computed-space: calc(var(--step) * var(--unit, 0.5rem));
  result: var(--computed-space);
}

.function-scene-scope {
  display: grid;
  gap: 1rem;
  min-inline-size: 0;
  font: 1rem/1.5 system-ui, sans-serif;
  overflow-wrap: anywhere;

  .sample {
    padding: 1rem;
    border: 1px solid currentColor;

    p {
      margin: 0;
      border: 1px dashed currentColor;
    }
  }

  > p {
    margin: 0;
  }

  .native {
    display: none;
  }

  .sample {
    --unit: 0.5rem;
    --step: 99;
  }

  .large {
    --unit: 1rem;
  }

  @supports (padding: --guide-local-space()) {
    .sample {
      padding: --guide-local-space();
    }

    .native {
      display: block;
    }

    .fallback {
      display: none;
    }
  }
}

--computed-space only exists during evaluation. Avoid local names that accidentally shadow parameters, and pass explicit arguments when you want dependencies to be visible. Variable scope does not turn an @function definition into a selector-local rule; use distinctive names to avoid collisions between definitions.

result and conditional branches

The last applicable result wins; there is no early return as in JavaScript.

Place the general result first and the conditional one after it. In this example @media changes the value when the viewport is narrower than 700px. Moving the general result to the end overrides the branch even when its condition matches. You can also vary local variables and resolve a single result at the end.

Responsive spacing with two results

Viewport < 700px: 0.75rem; from 700px: 1.5rem

Native CSS function: compare spacing with the expected result.

Base fallback: 1rem padding; call syntax is unavailable.

With native support, padding changes from 1.5rem to 0.75rem below a 700px viewport. Without support, it stays at 1rem. Resize the window to compare.
Responsive spacing with two results
<div class="function-scene-conditions">
  <div class="sample"><p>Viewport &lt; 700px: 0.75rem; from 700px: 1.5rem</p></div>
  <p class="native">Native CSS function: compare spacing with the expected result.</p>
  <p class="fallback">Base fallback: 1rem padding; call syntax is unavailable.</p>
</div>
@function --guide-responsive-space(--narrow <length>, --wide <length>: 1.5rem) returns <length> {
  result: var(--wide);

  @media (width < 700px) {
    result: var(--narrow);
  }
}

.function-scene-conditions {
  display: grid;
  gap: 1rem;
  min-inline-size: 0;
  font: 1rem/1.5 system-ui, sans-serif;
  overflow-wrap: anywhere;

  .sample {
    padding: 1rem;
    border: 1px solid currentColor;

    p {
      margin: 0;
      border: 1px dashed currentColor;
    }
  }

  > p {
    margin: 0;
  }

  .native {
    display: none;
  }

  @supports (padding: --guide-responsive-space(0.75rem)) {
    .sample {
      padding: --guide-responsive-space(0.75rem);
    }

    .native {
      display: block;
    }

    .fallback {
      display: none;
    }
  }
}

The condition checks the viewport of the document running the function, not the card width. In the playground, the function lives inside an iframe: @media checks that iframe. @supports and other allowed conditional rules have their own compatibility; this example only demonstrates @media. if() is another way to choose a value and is not needed for this recipe.

Comparisons

Choose an abstraction based on what actually repeats.

Use a custom property for a token or a value configured through the cascade and inheritance. For a simple calculation, calc() with variables on the component is often enough. @function adds a reusable signature with arguments, types, defaults, and logic that can return different results for each call.

A simple custom property alternative

A simple custom property alternative
CSS
.spacing-component {
  --step: 2;
  padding: calc(var(--step) * 0.75rem);

  &.compact {
    --step: 1;
  }
}

In this alternative, the formula is computed on the element receiving padding. If you define a composite value on :root and inherit it, its references resolve there: changing a dependency on a descendant does not automatically recompute that inherited value. A function computes at the call site. Neither option needs JavaScript.

A guarded fallback

Separate missing syntax from a function that is accepted but returns an invalid value.

All three previews define padding: 1rem outside the enhancement block. They only apply the call inside @supports (padding: --name()). In an engine without custom function call syntax, the condition fails and the base stays intact. This test checks syntax acceptance: it does not verify that the name exists, its arguments, or its result.

Counterexample: the earlier declaration does not always save you

Deliberately incorrect example, with no preview: if the engine accepts the call but the function does not exist, padding is invalid at computed-value time and uses its initial value, 0; it does not return to 1rem.

Counterexample: the earlier declaration does not always save you
CSS
.unprotected {
  padding: 1rem;
  padding: --missing-function();
}

The guard does not rescue a broken call in a supporting browser either. Check the computed value and test real arguments and branches. var(--token, alternative) is not a try/catch for every error in a property. The examples test padding with @supports to avoid also requiring at-rule(@function) support.

Common mistakes

Diagnose the signature, resolution, and destination property separately.

The conditional branch never changes the result

Expected result
The value adapts when @media matches.
Cause
A later general result overrides the conditional one; result does not stop evaluation.
Solution
Put the base first and exceptions after it, or change locals and compute result at the end.

The function exists, but the property is not applied

Expected result
The result matches both its declared type and the receiving property.
Cause
A required argument, returns, or the destination grammar does not match; returning a length does not make it valid for color.
Solution
Check arguments/defaults, the output type, and computed value. Test a minimal call before adding conditions.

A variable returns another value or is missing outside

Expected result
Each name resolves in its intended scope.
Cause
A local or parameter shadows the element property, or you are trying to read a local from outside the function.
Solution
Rename variables to avoid collisions and return data through result. Do not expect locals to be inherited in the DOM.

The fallback disappears in a supporting browser

Expected result
The interface keeps a useful value with and without the enhancement.
Cause
The call passes @supports but fails at computed-value time: the browser does not restore the previous declaration.
Solution
Keep a base outside the guard and also verify the call, values, and branches in the engine that accepts it.

Support and compatibility

The overall status does not certify every signature, branch, or returned value.

@function has limited availability. On October 22, 2026, Web Platform DX lists Chrome and Edge support from 139, with no stable Firefox or Safari support. MDN BCD marks Safari as preview; that does not mean a stable release is available.

The Baseline card reports the overall feature. Each example notice uses a local syntax test; the described measurements require its function to evaluate correctly. Fallbacks keep readable spacing without simulating functions in JavaScript. Review the linked data before production use: the draft and implementations can change.