Skip to content

Design tokens

Edit this page

Tokens are the named values your styles use: colors, spacing, font sizes and so on. You declare them once in the bundle configuration. css() only accepts their names. Each one becomes a CSS variable.

Raw tokens

tokens holds raw values, grouped by category:

1
2
3
4
5
6
7
8
9
10
11
12
13
# config/packages/ux_css.yaml
ux_css:
    tokens:
        colors:
            blue: { 500: '#3b82f6', 600: '#2563eb' }
            gray: { 50: '#f9fafb', 900: '#111827' }
            white: '#ffffff'
        spacing: { sm: '0.5rem', md: '1rem', lg: '2rem' }
        sizes: { prose: '65ch' }
        radii: { md: '0.375rem', full: '9999px' }
        fontSizes: { sm: '0.875rem', lg: '1.125rem' }
        fonts:
            body: ['Inter', 'sans-serif']

The categories are fixed, and are the same as in Panda CSS. Token categories lists the properties that take each one and the shape of their values. Inside a category, tokens can be nested in groups: blue.500 is the 500 token of the blue group. A fonts or shadows token can be a list, which is joined with commas. An easings token can be the four numbers of a cubic-bezier(). A shadows, borders, gradients or assets token can be an object under the value key.

A group can also have a value of its own, under the DEFAULT key:

1
2
3
4
5
# config/packages/ux_css.yaml
ux_css:
    tokens:
        colors:
            bg: { DEFAULT: '#ffffff', muted: '#f9fafb' }

Here, bg is the group itself, and bg.muted is the token inside it:

1
<div class="{{ css({ bg: 'bg', color: 'bg.muted' }) }}">

A token can also be written in the form Panda CSS uses, with a value key and an optional description key. UX CSS reads the value and ignores the description:

1
2
3
4
5
# config/packages/ux_css.yaml
ux_css:
    tokens:
        spacing:
            sm: { value: '0.5rem', description: 'Gap between inline items' }

In css(), a token is its name as a string:

1
<p class="{{ css({ color: 'blue.500', p: 'md', maxW: 'prose', fontFamily: 'body' }) }}">

Default tokens

default_tokens: panda adds the default tokens of Panda CSS: a palette of colors from red.50 to red.950 and more, a spacing scale from 0 to 96, and sizes, radii, font sizes, font weights, line heights, letter spacings, fonts, shadows, borders, blurs, aspect ratios, animations, durations and easings. The Symfony Flex recipe enables it.

1
2
3
4
5
6
7
# config/packages/ux_css.yaml
ux_css:
    default_tokens: panda
    tokens:
        colors:
            brand: '#4f46e5'
            red: { 500: '#e11d48' }

Your tokens are added to the default ones. A token with the same path replaces the default one: here, red.500 changes, and the other red shades stay.

Semantic tokens

semantic_tokens gives names to other tokens, by reference:

1
2
3
4
5
6
7
# config/packages/ux_css.yaml
ux_css:
    semantic_tokens:
        colors:
            primary: '{colors.blue.500}'
            fg: { base: '{colors.gray.900}', _dark: '{colors.gray.50}' }
            bg: { base: '{colors.white}', _dark: '{colors.gray.900}' }

A reference is written {category.path}. It can point to a raw token or to another semantic token.

A value with a base key is a conditional value. base applies by default. Each other key is a condition, such as _dark, that applies another value. Templates use the semantic name and never repeat the condition: color: 'fg' follows dark mode by itself. Conditions and breakpoints lists the available conditions.

A reference also works inside a longer value, and in raw tokens too:

1
2
3
4
5
# config/packages/ux_css.yaml
ux_css:
    tokens:
        shadows:
            focus: '0 0 0 3px {colors.blue.500}'

The shadow is written as 0 0 0 3px var(--colors-blue-500), so it follows the color.

Which properties take which tokens

Each property takes the tokens of one category. The mapping comes from Panda CSS and cannot be changed: see Token categories.

Tokens as CSS variables

Every token becomes a CSS variable in the tokens layer of the generated file, whether a template uses it or not. The name is the category followed by the path, with dashes:

1
2
3
4
5
6
7
8
9
10
11
12
13
@layer tokens {
    :where(:root, :host) {
        --colors-blue-500: #3b82f6;
        --spacing-md: 1rem;
        --fonts-body: Inter, sans-serif;
        --colors-primary: var(--colors-blue-500);
        --colors-fg: var(--colors-gray-900);
    }

    .dark {
        --colors-fg: var(--colors-gray-50);
    }
}

A semantic token refers to the variable of its target. A conditional value is written under the selector of its condition. The breakpoints are written as variables too (--breakpoints-md).

Hand-written CSS can use the same variables, so it matches the tokens:

1
2
3
4
5
/* assets/styles/app.css */
.prose a {
    color: var(--colors-primary);
    text-underline-offset: var(--spacing-sm);
}

Values that are not tokens

strict_tokens is enabled by default: a property bound to a category only accepts the tokens of that category. To use another value, write it between square brackets:

1
<div class="{{ css({ w: '[37ch]', color: '[#ff6600]' }) }}">

The brackets give the classes w_[37ch] and c_[#ff6600]. The value is written to the CSS as is. With strict_tokens: false, any string is accepted without brackets, as in Panda CSS by default.

A category that has no token at all accepts any value that the CSS property accepts, as in Panda CSS. For example, borderWidth: '1px' and opacity: '0.5' work when no borderWidths or opacity token is declared.

A few more forms are accepted wherever a token is expected:

  • bg: 'primary/50' mixes the color with 50% transparency, using color-mix();
  • mt: '-md' gives the negative value of a spacing token;
  • gap: 'var(--card-gap)' uses a CSS variable of your own.

Tokens inside a longer value

In css(), token(category.path) writes the CSS variable of a token inside a longer value. A second argument is a fallback:

1
<hr class="{{ css({ border: '1px solid token(colors.blue.500, gray)' }) }}">

This gives:

1
border: 1px solid var(--colors-blue-500, gray);

A property that takes a category needs the value between square brackets:

1
<input class="{{ css({ _focus: { shadow: '[0 0 0 3px token(colors.blue.500)]' } }) }}">

In the key of an at-rule, token() is replaced by the raw value of the token, because a media query cannot read a CSS variable:

1
<aside class="{{ css({ '@media (min-width: token(sizes.prose))': { display: 'block' } }) }}">

This gives @media (min-width: 65ch).

The {category.path} form of the configuration is not accepted in css(): a value cannot contain braces. Use token() instead.

Configuration errors

The token configuration is checked when the container is built, so cache:clear reports mistakes before any template is rendered:

Mistake Message
Unknown category Unknown token category "color". Did you mean "colors"?
Reference to a token that does not exist The "colors.primary" token references the unknown token "colors.rde". Did you mean "colors.red"?
Tokens that refer to each other Circular token reference: "colors.a -> colors.b -> colors.a".
Unknown condition in a conditional value The "colors.fg" token uses the unknown condition "_drak". Did you mean "_dark"?
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version