[csswg-drafts] [css-values-5] A way to dynamically construct function calls (`<dashed-function>` etc) (or wrap values in arbitrary tokens) (#12806)

LeaVerou has just created a new issue for https://github.com/w3c/csswg-drafts:

== [css-values-5] A way to dynamically construct function calls (`<dashed-function>` etc) (or wrap values in arbitrary tokens) ==
Prior art / related: 
-  #10006 
- #9141
- #12219 

We already [have](https://drafts.csswg.org/css-values-5/#ident) `ident()` to construct `<ident>` values, however there is still no path from an `<ident>` to a function call, which comes up frequently, and will come up even more frequently once we have mixins and custom functions (see Use cases below).  

For context (@tabatkins feel free to correct me if anything here is wrong), since `<function-token>` includes _both_ the function name and the opening paren, something like this:

```css
--foo: linear-gradient;
background: ident(var(--foo))(white, black);
```

…would be parsed as an `<ident> <(-paren> <ident> <ident> <)-token>` and not as the intended `<function-token> <ident> <ident> <)-token>`.

Now we don't _actually_ want a `<function-token>` factory in CSS, as that would be incredibly awkward and would break custom property parsing. Instead, we probably want a way to construct the entire function call by passing an `<ident>` name and a `<declaration-value>` for the argument(s).

Perhaps, something like:

```
<call()> = call( <custom-ident>, <declaration-value>)
```

Alternative names: `function-call()`, `function()`, `apply()`

This would operate on the syntax level: it would literally construct a `<function-token>` from the ident provided, consume a comma, add all other tokens after it, tuck a `<)-paren>` at the end, done. Usual IACVT behavior if cycles, invalid etc.
My hope is that making this a syntax-level feature akin to `ident()` could circumvent a lot of the issues around defining e.g. a mixin-specific feature AND allow it to address multiple other pain points.

Alternatively, we can skip the comma, but with a comma we could later expand to other prelude parameters.

## Example Use cases 

### Dynamic functions and mixin calls

#10006 is an obvious use case, and by far the most important one. In theory, dynamically calling mixins without parameters is already possible via `ident()`, but this would allow calling mixins with parameters as well.

#### Passing design systems parameters to components

This can be a game-changer for components & design systems (#10948), as pages can simply 
- Define mixins with their core styles (e.g. a `--button` mixin) and then pass them to components in one go (e.g. `--button-styles: --button`) instead of painfully communicating multiple properties (also doubling as a more flexible solution for the problems #9992 was trying to solve)
- The properties linking to different mixins could *also* be a mixin themselves, essentially making it possible to describe an entire design system in one declaration, eliminating or significantly reducing the pain that drove #10948 and #10222 😀

#### Pave the cowpaths for block `@if`

This could also pave the cowpaths for block `@if` without block `@if` 

We already have `if()`, and we've resolved to accept `revert-rule` in https://github.com/w3c/csswg-drafts/issues/10443#issuecomment-2627865962 . Together with this, it means we can finally have block conditionals:

```css
.callout.note {
 /* conditionally apply mixin without encoding the condition in the mixin */
 @apply call(if(style(--is-foo: bar): --bar-mixin));
}
```

This would also eliminate the need for scoped mixins or functions, since CSS variables are scoped, so the scoping can be emulated through references.

### Encapsulation / currying

In design discussions we often bring up arguments such as "`foo-bar(<args>)` and `foo(bar <args>)` are not meaningfully different".
Except, currently, they are. Because in the latter, `bar` can become part of a variable that is passed around, but in the former that is not the case.
E.g. you can define a relative color transformation fully in a variable and then apply it via `color(from var(--some-color) var(--transformation))` *provided* the color space is a `color()` color space. If it's a color space that can only expressed as a function, you cannot do that, every usage point needs to know and repeat the function name.

However, often the function itself is part of the information we want to encapsulate.
Suppose we wanted to implement darkening shades via `color-mix(in oklab, <color>, black 10%)` or via `oklab(from <color> calc(l - 0.1) c h)` etc. Currently, turning this into variables would still require that authors using the variables know what general algorithm is used to darken colors:

Method 1:
```css
:root {
 --darker-prefix: in oklab, 
 --darker-suffix: , black 10%;
}

.foo {
 background: color-mix(var(--darker-prefix) var(--color) var(--darker-suffix);
}
```

Method 2:
```css
:root {
 --darker: calc(l - 0.1) c h;
}

.foo {
 background: oklab(from var(--color) var(--darker));
}
```

With `call()`, the calling point doesn't need to know _how_ colors are darkened — it can become an implementation detail:

Method 1:
```css
:root {
 --darker-prefix: color-mix, in oklab;
 --darker-suffix: , black 10%;
}

.foo {
 background: call(var(--darker-prefix) var(--color) var(--darker-suffix));
}
```

Method 2:
```css
:root {
 --darker-prefix: oklab, from ;
 --darker-suffix: calc(l - 0.1) c h;
}

.foo {
 background: call(var(--darker-prefix) var(--color) var(--darker-suffix)); /* same! */
}
```

Of course, simply encapsulating is done more elegantly with custom functions. The power of something like this is that the actual encapsulated transformations can *cascade* just like regular variables. For example, `--darker` may have a different meaning in different contexts, in dark mode, within a callout, etc.

In some cases we can emulate some of that through functions that reference CSS variables on the element, but not in all.

### Graceful degradation for new CSS functions

This also comes up when using features in a PE way. E.g. for a feature like gradient color space interpolation, you can do:

```css
:root {
 --in-oklab: ;
 @supports (background: linear-gradient(in oklab, white, black) { --in-oklab: in oklab; }
}
```

And then define gradients like `linear-gradient(to right var(--in-oklab), white, black)` and get graceful degradation almost for free.

However, if the feature is a new CSS function we cannot do that. E.g. adopting `light-dark()` currently is painful if one needs broad browser support, because there is no easy way to fall back to something reasonable without duplicating every single color. And given the amount of color declarations a design system needs to define, that is a significant issue.

A lot of this can also be addressed via custom functions, but with something that cascades, one can choose different fallbacks for different contexts.

## Alternative design: `use()` to wrap in arbitrary tokens

Since this operates on a syntax level, perhaps we could broaden it even more. Why restrict to wrapping in `<function-token>` and `<)-token>` when you can wrap in *any* arbitrary tokens? 😀 

Then it could become like a mini cascading function of sorts, but with CSS variable scope and a single predefined argument (or even multiple, via `nth-item()` (#11103)).
The challenge is that then we need a special token to substitute the value into with `var()` semantics but without naming conflicts, e.g. `var(args)`:

```
<use()> = use([ <declaration-value> | 'var(args)' ]+)
```

Used like (note how much more elegant the call is compared to the `call()` version): 
```css
:root {
 --darker: oklch( from var(args) calc(l - 0.1) c h);
}

.foo {
 background: use(var(--darker) var(--color));
}
```

Though it seems that even if we pursue this route, there might still be value in `call()` as a more straightforward MVP.

## Challenges

- Is `ident()` actually allowed in `@apply`? See https://github.com/w3c/csswg-drafts/issues/12219#issuecomment-3293446258 If not, the same problems would apply here too.

Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/12806 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Tuesday, 16 September 2025 00:31:50 UTC