Every visual value in a Prime component comes from a design token. A token is a named value in a preset, the theme object such as the built-in Aura, and each one is written to the page as a CSS variable with a mechanical name: the dotted path in kebab case after the --p- prefix. primary.color becomes --p-primary-color. A camel-cased key in the object splits into segments, so formField.paddingX is the token form.field.padding.x and the variable --p-form-field-padding-x. The component CSS reads the variables and no raw values, so changing a token changes every component that reads it, without touching the component styles.

The examples apply to @primeuix/styled 1.1.0 and later with @primeuix/themes 3.0.2 and later. PrimeVue 5.0.3 and later, PrimeReact 11.2.1 and later, and PrimeNG 22.1.3 and later install them. The CSS shown was produced by running the examples against those packages.

Three layers

A preset is an object with three token sections: primitive, semantic and components.

Primitive tokens are raw values with no meaning attached. In Aura the section holds the border radius scale and 22 color scales, emerald, blue, slate and the rest, each from 50 to 950:

primitive: {
    borderRadius: { none: '0', xs: '2px', sm: '4px', md: '6px', lg: '8px', xl: '12px' },
    emerald: { 50: '#ecfdf5', 100: '#d1fae5', /* ... */ 500: '#10b981', /* ... */ 950: '#022c22' },
    blue: { /* ... */ }
}

Nothing in this layer says what a color is for. emerald.500 is a color, not “the primary color”.

Semantic tokens give the primitives a role. primary is the brand color scale and its working colors, surface is the gray scale for backgrounds and borders, text, content, overlay, navigation, list and formField carry the values that groups of components share, and a handful of tokens apply everywhere: focusRing, transitionDuration, disabledOpacity, iconSize, anchorGutter and typography. A semantic token references a primitive:

semantic: {
    primary: {
        50: '{emerald.50}',
        /* ... */
        500: '{emerald.500}',
        /* ... */
        color: 'light-dark({primary.500}, {primary.400})',
        contrastColor: 'light-dark(#ffffff, {surface.900})',
        hoverColor: 'light-dark({primary.600}, {primary.300})',
        activeColor: 'light-dark({primary.700}, {primary.200})'
    },
    formField: {
        paddingX: '0.625rem',
        paddingY: '0.375rem',
        borderRadius: '{border.radius.md}',
        /* ... */
    }
}

formField shows why the layer exists. Every form component, InputText, Select, Checkbox and DatePicker among them, reads its padding, radius, border and background from form.field.*, so one change there moves all of them together.

Component tokens are the values one component reads. Each component has its own subtree, and Aura has 97 of them. They reference the semantic layer:

components: {
    button: {
        root: {
            borderRadius: '{form.field.border.radius}',
            paddingX: '{form.field.padding.x}',
            gap: '0.5rem',
            primary: {
                background: '{primary.color}',
                hoverBackground: '{primary.hover.color}',
                color: '{primary.contrast.color}'
            },
            /* secondary, info, success, warn, help, danger, contrast */
        },
        outlined: { /* ... */ },
        text: { /* ... */ },
        link: { /* ... */ }
    },
    card: {
        root: {
            background: '{content.background}',
            borderRadius: '{border.radius.xl}',
            color: '{content.color}'
        },
        body: { padding: '1.125rem', gap: '0.5rem' }
    }
}

So the primary button’s background runs through a chain: button.primary.background reads {primary.color}, which reads {primary.500}, which reads {emerald.500}. In the page each link is its own variable, and only --p-emerald-500 holds a literal value:

--p-emerald-500: #10b981;
--p-primary-500: var(--p-emerald-500);
--p-primary-color: light-dark(var(--p-primary-500), var(--p-primary-400));
--p-button-primary-background: var(--p-primary-color);

Which layer to change

Where a change goes depends on what it should affect:

  • A different brand color across the whole app is a semantic change: point primary at another scale, and every component that reads primary.* follows. The primitive scales stay as they are.
  • A color that the preset does not have is a primitive addition. Add the scale under primitive, then reference it from the semantic layer. A hex value written straight into semantic.primary.color works, but it has no scale behind it, so primary.50 and primary.600 no longer match it.
  • One component that should look different is a component change. A button with a larger radius is components.button.root.borderRadius, and no other component moves.
  • A value that several components should share, but that no semantic token covers yet, is a custom token under extend, covered further down.

Avoid copying a value across layers. A component token set to #10b981 matches the brand color today and keeps its copy when the brand color changes.

References and light-dark()

A reference is a dotted path in braces. It can point at any token in any layer, and it is written to CSS as a var() reference to that token’s variable:

semantic: {
    formField: { borderRadius: '{border.radius.lg}' }
}
--p-form-field-border-radius: var(--p-border-radius-lg);

A value that differs between light and dark mode is written with the CSS light-dark() function, and references work inside it the same way:

primitive: {
    brand: { 400: '#4f8cff', 500: '#2f6fe6', 600: '#1f54b8' }
},
semantic: {
    primary: {
        color: 'light-dark({brand.500}, {brand.400})',
        hoverColor: 'light-dark({brand.600}, {brand.500})'
    }
}
--p-brand-400: #4f8cff;
--p-brand-500: #2f6fe6;
--p-brand-600: #1f54b8;
--p-primary-color: light-dark(var(--p-brand-500), var(--p-brand-400));
--p-primary-hover-color: light-dark(var(--p-brand-600), var(--p-brand-500));

The theme does not split the pair into two rules. It writes one declaration, and the browser picks the first or second value from the color-scheme of the element. The theme sets that property from the darkModeSelector option. With the default, the root is color-scheme: light and a prefers-color-scheme: dark media query switches it to dark. With a class such as .app-dark, the class sets color-scheme: dark and everything under it flips. So one declaration covers both modes, and a token with the same value in both modes does not use light-dark().

References also work inside other CSS functions. Aura’s highlight background mixes the primary color with transparency in dark mode:

highlight: {
    background: 'light-dark({primary.50}, color-mix(in srgb, {primary.400}, transparent 84%))'
}
--p-highlight-background: light-dark(var(--p-primary-50), color-mix(in srgb, var(--p-primary-400), transparent 84%));

Because the output is a reference, a light-dark() pair built on {primary.*} follows a later change to primary in both modes without being rewritten.

Custom tokens with extend

extend is the place for tokens the built-in tree does not have. It keeps them apart from the built-in keys, and it is what the Theme Designer’s custom tokens and the Figma kit’s Custom collection map to, both covered at the end. It exists at the top level and inside each component:

import { definePreset } from '@primeuix/themes';
import Aura from '@primeuix/themes/aura';

const MyPreset = definePreset(Aura, {
    extend: {
        accent: {
            color: '{amber.500}',
            contrastColor: '#ffffff'
        }
    },
    components: {
        button: {
            extend: {
                accent: {
                    background: '{accent.color}'
                }
            }
        }
    }
});

A top-level token becomes a variable with the plain prefix, and a component token carries the component’s name, next to the built-in tokens of that component:

:root,
:host {
    --p-accent-color: var(--p-amber-500);
    --p-accent-contrast-color: #ffffff;
}

/* in the button's variable block */
--p-button-accent-background: var(--p-accent-color);

Custom tokens can reference each other and the built-in tree, and a built-in token can reference them back. A new Button severity, a variant next to primary, secondary and danger, needs a custom token and a CSS rule that reads it. The rule goes under the css key.

CSS in the preset

A preset can carry CSS as well as tokens: a global block under css, and a block per component under components.<name>.css. Inside, a token is read with dt('path') written as text. The theme replaces each call with the variable reference when it writes the component’s style, so the preset stays a plain object:

const MyPreset = definePreset(Aura, {
    extend: {
        accent: { color: '{amber.500}', contrastColor: '#ffffff' }
    },
    components: {
        button: {
            extend: {
                accent: { background: '{accent.color}' }
            },
            css: `
.p-button-accent {
    background: dt('button.accent.background');
    color: dt('accent.contrast.color');
}
`
        }
    },
    css: `
.app-banner {
    background: dt('accent.color');
}
`
});
.p-button-accent {
    background: var(--p-button-accent-background);
    color: var(--p-accent-contrast-color);
}

The css value can also be a function that receives dt and returns the string, which is the form for template literals with conditions in them:

components: {
    button: {
        css: ({ dt }) => `
.p-button-accent {
    background: ${dt('button.accent.background')};
}
`
    }
}

Both forms produce the same CSS. The component block is written together with that component’s styles, so it is loaded when the first instance of the component mounts, inside the same cascade layer and with the same classPrefix handling. The global block is written with the global styles.

Server rendering is the one place this differs: PrimeVue 5.0.3 writes the rules from css in the browser and leaves them out of the server response, while the token variables are in it. The server-rendering setup is in Theming with server rendering in PrimeVue, PrimeReact and PrimeNG.

dt and $dt

dt is also a real function, exported by @primeuix/themes and @primeuix/styled, for CSS that an app builds in JavaScript. It takes the dotted path and returns the variable reference, with or without braces:

import { dt, $dt } from '@primeuix/themes';

dt('primary.color'); // 'var(--p-primary-color)'
dt('{primary.color}'); // 'var(--p-primary-color)'

The second argument is a fallback, and it goes into the var():

dt('button.border.radius', '6px'); // 'var(--p-button-border-radius, 6px)'
dt('accent.color', dt('primary.color')); // 'var(--p-accent-color, var(--p-primary-color))'

The fallback matters for a custom token. The rule that reads accent.color stays in the app, while the token exists only in the preset that defines it. When the app switches to a preset without that token, the rule points at a variable nobody declared. Without a fallback the declaration is invalid at computed-value time and the property takes its initial or inherited value, which for background is transparent and for color is the parent’s color, and neither looks like an error. With a fallback the rule still has a known value. A token the preset itself defines needs none.

The third argument asks for the resolved value instead of the reference, and $dt returns name, reference and value together:

dt('primary.color', undefined, 'value'); // 'light-dark(#10b981,#34d399)'

$dt('accent.color');
// { name: '--p-accent-color', variable: 'var(--p-accent-color)', value: '#f59e0b' }

'value' follows the reference chain to the end and folds a light and dark pair into one light-dark() string, so code that needs a single color still has to pick the scheme. Stylesheets keep the reference. The value form is for code that compares tokens or passes a color to something outside CSS.

The styled API in brief

@primeuix/styled is the package under the three libraries’ styled mode, the mode where components get their CSS from a preset, and @primeuix/themes re-exports its functions. The ones that touch tokens:

  • definePreset(base, overrides) merges the override tree into a base preset and returns a new one. Keys that are left out keep the base values, so a preset that changes only primary is a few lines.

  • usePreset(preset) replaces the active preset entirely. updatePreset(overrides) merges into it at runtime, for a change that should not rebuild the theme from scratch.

  • updatePrimaryPalette(scale) and updateSurfacePalette(scale) are the shortcuts for the two scales most apps change. They merge at the stop level, so a partial scale keeps the stops it does not name.

  • palette(color) builds a 50 to 950 scale. With a hex value it computes the tints and shades, with a reference it returns references:

    import { palette } from '@primeuix/themes';
    
    palette('#2f6fe6'); // { 50: '#f5f8fe', 100: '#cddcf9', ..., 500: '#2f6fe6', ..., 950: '#0c1c3a' }
    palette('{blue}'); // { 50: '{blue.50}', 100: '{blue.100}', ..., 950: '{blue.950}' }

    The first form is for a brand color with no scale behind it; the second maps primary onto a built-in scale.

  • mix(a, b, amount), shade(color, amount) and tint(color, amount) are the color helpers behind palette, available for a preset that builds its own values: shade('#ff8400', 20) is #cc6a00, tint('#ff8400', 20) is #ff9d33.

  • dt, $dt and the css tagged template, covered above.

The theme options and the dt prop are not functions, but they also act on tokens. The options, passed with the preset, decide how the variables are written: prefix renames --p-, darkModeSelector chooses how dark mode is switched, cssLayer wraps the output in a cascade layer, cssVariables: false writes resolved values instead of var() references, and classPrefix renames the p- classes. The dt prop on a component takes a token object rather than a path and scopes the values to that instance:

<ToggleSwitch v-model="checked" :dt="{ root: { checkedBackground: '{amber.500}' } }" />

The options are listed in each library’s styled mode guide, and the prop is documented there under scoped tokens.

Every preset also ships a compat variant, @primeuix/themes/aura-compat and so on, with the same tokens calibrated for a 14px root font size. The standard presets assume the browser default of 16px. The compat variants are for an existing application that still runs on 14px and are maintained until June 2027.

The same tokens in Figma

The PrimeOne UI Kit for Figma is built on the same tree. Version 4.0 and later keep the tokens in native Figma variables, and the current release is 5.0.0. The variable collections follow the preset layers: Primitive, then Semantic Common and Semantic Color Scheme, then Component Common and Component Color Scheme. A variable’s path is the token’s path: primary/500 in the kit is primary.500 in the preset, and the Semantic Common/form/field/padding/x variable is form.field.padding.x. The Color Scheme collections hold the values that differ per mode, which is the light-dark() pair on the code side.

Two collections sit outside the tree. App is for values that belong to the application rather than the design system, such as a page’s font size. Custom is for new variables, and it is the one the code side reads as custom tokens: a variable in Custom lands in the theme the way an extend token does. Changing or deleting variables in the default collections puts the kit out of step with the library variables.

The PrimeUI Theme Generator plugin moves the variables to code. It exports the selected collections and modes as a JSON token file. References stay references, and a color variable with its own opacity becomes a color-mix() expression. With the secret key from a Theme Designer account it also generates the preset files, and it can push tokens or theme files to a GitHub, GitLab or Bitbucket repository.

The Theme Designer is the editor in each library’s showcase. A theme starts from a built-in preset or from the plugin’s export through Import Figma Variables, and the editor shows one section per layer, with the component tokens on the page of each component. Typing { at the start of a value opens autocompletion over the token names. Custom tokens are added by dotted name, for example accent.color with #eab308 or {yellow.50}. When a newer library version adds tokens, the migration assistant adds them to the theme. The generated theme is a preset object like the ones above.

Component mapping is a separate step from token mapping: the plugin and the Designer carry values, while Code Connect tells Figma which component in the kit is which component in the code.

Reference

The theming guide of each library has the full token list per component and the option reference: PrimeVue, PrimeReact and PrimeNG. Each component’s token type can be imported, for example @primeuix/themes/types/button for Button, and typing a preset with it gives autocompletion in the editor. How the same preset is wired into Vue, React and Angular is in Theming across PrimeVue, PrimeReact and PrimeNG, and what changed in the packages themselves is in PrimeUIX styled 1.1.0 and themes 3.0.2.