Skip to content
TrademarkTrademark
Docs
Contexts

ThemeProvider

Updated: October 10, 2026
View on GitHub

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.

Simple setup

Wrap your app with ThemeProvider and pass the style configuration as props.

Advanced setup

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.

Custom color

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.

Reading and changing the theme

Access and modify theme state with useTheme:

SSR hydration

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.

ThemeInit

Type
Default
configrequired
The style section of once-ui.config, as written.
OnceUIStyleConfigThemeInitAttributeConfig
—
dataStyle
The dataStyle section; its mode becomes data-viz-style.
{ mode?: ChartMode }—

Style your app

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.

PageCustomize page theme
Theme
Shape
ColorCustomize color schemes
Brand
Accent
Neutral
Solid styleCustomize the appearance of interactive elements
Style
Effect
AdvancedCustomize advanced styling options
Surface
Scaling
Data Style
Transition

Style overrides

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.

Root font size and cursor

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.

Persistence

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.

API reference

ThemeProvider

Type
Default
theme
systemdarklight
system
defaultTheme
darklightsystem
system
neutral
sandgrayslaterosemintduskcustom
gray
brand
blueindigovioletmagentapinkredorangeyellowmossgreenemeraldaquacyancustom
blue
accent
blueindigovioletmagentapinkredorangeyellowmossgreenemeraldaquacyancustom
indigo
solid
colorcontrastinverse
contrast
solidStyle
flatplastic
flat
border
roundedplayfulconservativesharp
playful
surface
filledtranslucent
filled
transition
allmicromacronone
all
scaling
9095100105110
100
bodySize
9095100105110
100
bodyLineHeight
90100110120
100
desktopSize
Root font size in px above 1024px. Unset, the stylesheet's 16px applies.
131415161718
—
tabletSize
Root font size in px from 769px to 1024px. Unset, the stylesheet's 16px applies.
131415161718
—
mobileSize
Root font size in px at 768px and below. Unset, the stylesheet's 15px applies.
131415161718
—
cursorInteractive
The cursor over interactive elements. Unset, the stylesheet's pointer applies.
pointerdefault
—
cursor
A cursor preset for the whole app. Unset, the browser's own cursors.
softroundeddotsystem
—
brandSeed
A colour to generate the brand scheme from. Implies brand="custom" when brand is not set.
string—
accentSeed
As brandSeed, for the accent scheme.
string—
neutralSeed
As brandSeed, for the neutral scheme.
string—
brandScale
The brand scheme's steps, once or { light, dark }. Implies brand="custom" when brand is not set.
ThemedSchemeScale—
accentScale
As brandScale, for the accent scheme.
ThemedSchemeScale—
neutralScale
As brandScale, for the neutral scheme.
ThemedSchemeScale—
pageBackground
The page background: a token or CSS colour, once or { light, dark }.
ThemeColor—
surfaceBackground
The filled surface's background, as pageBackground.
ThemeColor—
children
React.ReactNode—
persistence
"local" | "none" | StylePersistenceAdapter"local"

useTheme

Returns { theme, resolvedTheme, setTheme }.

Type
Default
theme
Current theme setting
Theme—
resolvedTheme
Actual resolved theme
"light" | "dark"—
setTheme
Update theme
(theme: Theme) => void—

useStyle

Returns all style options plus setStyle.

Type
Default
theme
Theme—
neutral
NeutralColor | "custom"—
brand
Schemes | "custom"—
accent
Schemes | "custom"—
solid
SolidType—
solidStyle
SolidStyle—
border
BorderStyle—
surface
SurfaceStyle—
transition
TransitionStyle—
scaling
ScalingSize—
bodySize
BodySize—
bodyLineHeight
BodyLineHeight—
desktopSize
DeviceFontSize | undefined—
tabletSize
DeviceFontSize | undefined—
mobileSize
DeviceFontSize | undefined—
cursorInteractive
InteractiveCursor | undefined—
cursor
CursorPresetValue | undefined—
setStyle
(style: Partial<StyleOptions>) => void—
Edit this page on GitHub