The ThemeProvider is a context that persists user theme preferences and syncs with the system’s light/dark setting. It provides a useTheme hook to manage theme state at runtime and sets data attributes on the <html> element.
Wrap your app with ThemeProvider and pass the style configuration as props.
Keep every style choice in one file, typed with defineConfig, and spread it into the provider. The keys are the provider's prop names, so nothing is cast or renamed. We also recommend a Providers component to wrap your app.
A 2.0 Providers that casts each value (brand={style.brand as Schemes}) still compiles; defineConfig is what makes the casts unnecessary.
If you don't pass any props to the ThemeProvider, it will fall back to defaults. If the visitor has theme preferences (saved in localStorage), the ThemeProvider will use those. To force a theme, set theme on the ThemeProvider as light or dark. A forced theme ignores setTheme, so a theme switch stops working. To start visitors on light or dark and still let them switch, leave theme as system and set defaultTheme instead; ThemeInit reads it too, so the first paint is right.
The quickest way to a custom palette is one colour. Give brandSeed, accentSeed or neutralSeed a hex, rgb() or oklch() value and the whole 12-step scheme is generated from it. A seed implies "custom" when you have not named the scheme yourself. For exact colours from a brand book, give brandScale, accentScale or neutralScale the steps themselves, { 100: "#…", …, 1200: "#…" }; a seed fills any step the scale leaves out.
ThemeInit generates the scheme on the server and renders it into a <style> next to its script, so the page paints with the custom brand colour from the first frame. ThemeProvider applies the same values on the client, which is what lets a seed change at runtime, for example in an editor previewing a colour. A seed that cannot be parsed is ignored, and the scheme keeps its default rather than pointing at nothing.
A ramp tuned for a light page often reads wrong on a dark one. Give a scale per theme as { light: { … }, dark: { … } }; a theme you leave out uses the other's, and a seed still fills missing steps. ThemeInit renders both, keyed off data-theme, so the first paint is right in either theme and a theme="dark" section on a light page gets the dark ramp.
The page background and the filled surface (cards, menus, dialogs) take a token or any CSS colour through pageBackground and surfaceBackground, once for both themes or { light, dark }. A theme you leave out keeps the default: in light mode an off-white page under white surfaces.
To hand-pick every step instead, set the scheme to custom and supply the --scheme-* variables yourself. You can generate palettes using the Once UI customize page.
Access and modify theme state with useTheme:
Add the <ThemeInit> component to the <head> to set the initial theme before hydration in layout.tsx. It will prevent flash of unstyled content (FOUC). It takes the same style object as the provider, plus dataStyle for the chart mode:
Each key is written to <html> as data-<kebab-key> (solidStyle becomes data-solid-style), and dataStyle.mode as data-viz-style. A choice the visitor saved, through a StylePanel for example, is applied on top before the first paint.
The 2.0 form, with attribute names as keys ("solid-style": style.solidStyle), is still accepted. Its "viz-style" key is the chart mode: the 2.0 docs filled it from dataStyle.variant, which matches no style, so charts fell back to categorical. Passing dataStyle reads dataStyle.mode instead.
Visit the ThemeSwitcher or StylePanel page for native, out-of-the-box ThemeProvider implementation with localStorage persistence. Use these components to provide user-facing style controls or to quickly test different styles during development.
The ThemeProvider relies on data attributes to set theme and style preferences. Data modes are inherited from the parent and can be applied to any semantic HTML element, providing simple, seamless style overrides.
Just add the data-theme, data-neutral, data-brand, data-accent, data-border, data-surface, data-solid, data-solid-style, data-transition or data-scaling attributes to any semantic HTML element to override its default style.
On Flex, Row, Column and Grid, the four you reach for most are typed props, checked against the same values as this provider: theme writes data-theme, themeBorder writes data-border, themeSolid writes data-solid and themeSurface writes data-surface. border and solid already mean colours on those components, hence the prefix.
desktopSize, tabletSize and mobileSize set the root font size, in px, for each width range, which is what every rem in the system resolves to. cursorInteractive sets the cursor over buttons, links and other interactive elements. They have no defaults of their own: left unset, nothing is written to <html> and the stylesheet's values apply (16px, 16px, 15px and pointer).
They belong in the style section of the config like any other key, so ThemeInit applies them before the first paint.
cursor sets a cursor preset for the whole app: "soft", "rounded" or "dot", each with its own pointer, text and disabled states over the controls. Unset or "system", the browser's own cursors. A Flex or Grid can set another one for its subtree: see Flex.
Style choices are mirrored into localStorage under data-<kebab-key>. Set persistence to change that:
- "local" — the default described above.
- "none" — choices apply to the document but are not stored. Use this when a host owns the state, such as an editor holding an unsaved draft.
- an adapter — route persistence somewhere else, such as a database.
get runs during hydration, so it must be synchronous — an await there would show a flash of the wrong theme. When the values live somewhere async, omit get and seed the provider through its props from the server instead. set and remove may return a promise; nothing waits on them.
Returns { theme, resolvedTheme, setTheme }.
Returns all style options plus setStyle.