Once UI's color system is designed to be accessible and consistent, allowing you to create harmonious, functional interfaces. It consists of two layers: the base layer that contains all possible color tokens and the semantic layer that aliases the base tokens to give them meaningful names.
In Once UI, we call the different color spaces schemes. Color tokens are constructed from pairing a scheme with a weight, such as sand-700. Base tokens serve as the foundational layer of the color system and they are not meant to be used directly.
Schemes provide a comprehensive and consistent initial color palette. Each scheme has 12 weights (100–1200), giving you fine-grained control over contrast and tone:
Semantic tokens are contextual - they adapt based on your theme configuration. Each of the 7 semantic schemes provides tokens for backgrounds, text, solids, borders, and overlays across the strong, medium, and weak weight variants.
Beyond the semantic and function token systems, Once UI provides several specialized color token groups for specific UI concerns.
Colour arrives in pairs. background sets a surface and onBackground sets the text that sits on it; solid sets a filled control and onSolid the text on that. Setting the text colour on the container means every label inside inherits it, so it is written once rather than on each child.
Which to reach for:
- background for a panel, a section, a page area.
- solid for something interactive that has to read as pressable — a button, a checkbox, a filled badge.
- an alpha weight when the colour should tint whatever is behind it rather than replace it. This is what keeps a layer working over both an image and a plain surface.
- surface for the neutral raised panel, which follows the theme without naming a scheme at all.
solid="contrast", the default solid style, draws filled controls in neutral: near-white in dark mode, near-black in light. So under it --brand-solid-strong is not the brand colour, by design, and anything that reads it for "the brand colour" loses its hue.
When something needs the brand hue whatever the solid style, such as an effect, an illustration, a gradient or a highlight, use --brand-color-strong, --brand-color-medium or --brand-color-weak. They are the brand solids as data-solid="color" draws them, in both themes, and no solid style changes them.
Keep --brand-solid-* for what it is for: filled, pressable things that should follow the solid style. --brand-on-background-* keeps the hue too, but is tuned for text on a background, so it runs light in dark mode.
Scheme tokens are oklch(). The values are the same colours the palette has always used — every one round-trips to the hex it replaced — but expressed in a space where the numbers mean something: oklch(0.6743 0.1670 261.54) reads as "two thirds as light as white, moderately saturated, blue", where #5A93FC reads as nothing.
That needs Chrome 111, Safari 15.4 or Firefox 113, all from 2022–23. There is no fallback: a browser that does not understand oklch() drops the declaration rather than approximating it.
data-brand="custom" reads --scheme-brand-* (and data-accent and data-neutral the same way), which your app supplies. Rather than hand-picking twelve steps that sit evenly against each other, generate them from one colour. In the config, that is a seed:
ThemeInit generates the scheme on the server and renders it in a <style>, so it is there before the first paint; ThemeProvider applies the same values on the client when the seed changes. See ThemeProvider.
To apply a scheme yourself at runtime, customSchemeProperties returns the properties to write, from the same pure function both of those use:
generateColorScheme and schemeAlphaVariants underneath it return the raw twelve steps and the three alphas, if you need the values for something other than the page's scheme.
The seed can be hex, rgb()/rgba() or oklch() — which covers what ColorInput produces with and without supportAlpha. An unparseable seed returns null rather than something wrong, so you can leave the current scheme in place.
Steps are anchored to a contrast ratio against black, not to fixed lightness values, so a generated ramp holds the same contrast relationships as the built-in schemes whichever hue it starts from. Hue and chroma come from the seed and stay put; only lightness moves.
One thing it does not do is copy the built-in schemes' hand-tuning. Those were drawn by eye, and at the saturated dark end — step 400 especially — several carry more chroma than their own step 600. A ramp built from a single seed cannot know that. Regenerating each built-in scheme from its own step 600 lands within 0.11 OKLab ΔE at the worst step, and inside 0.05 for most.