TrademarkTrademark
Docs
Ctrl k
Search docs...
Components

StylePanel

Updated: October 11, 2026
View on GitHub

StylePanel is a component to set global styling options. It writes to the root HTML element’s data-* attributes to affect global styles in real time. It doesn't persist settings by default.

Themes and shape

Choose between light/dark mode and various border radius presets.

Color systems

Change brand, accent, and neutral palettes. These affect buttons, links, surfaces, and other themed elements.

Solid style and effects

Adjust the visual language of interactive elements (e.g., plastic vs flat button styling).

Advanced options

Modify surface behavior (translucent vs filled), scale intensity, and transition timing.

Global output

Applies the following data-* attributes to the <html> element:

  • data-border
  • data-brand
  • data-accent
  • data-neutral
  • data-solid
  • data-solid-style
  • data-theme
  • data-transition
  • data-scaling
  • data-surface
  • data-body-size
  • data-body-line-height

Composing your own panel

StylePanel is a plain composition of its own parts. When you need a different arrangement, or only some of the sections, drop it and compose the pieces inside StylePanel.Root:

Rows: Theme, Shape, Brand, Accent, Neutral, Solid, Effect, Surface, Scaling, Transition, DataStyle, BodySize, BodyLineHeight. Groups: Page, Color, SolidStyle, BodyText, Advanced.

Only DataStyle needs a DataThemeProvider in the tree, so a panel without it has no such requirement.

Every row takes a label, and every group a title and description. Pass a node to replace the default, or false to drop it:

To translate the panel, pass labels to the root instead — see Localisation. A prop on a row or group still wins over it.

To keep the default arrangement and hide parts of it, use visibility. Omitted keys stay visible; section: false hides a whole group.

bodyText is the exception: it is opt-in, so pass { bodyText: { section: true } } to show it.

Narrow panels

Each row puts its label beside its control, which suits a page or a settings screen. In a sidebar there is no room for both, so the colour strips scroll after a few swatches. stacked puts every label above its control instead:

It is a prop rather than automatic because the panel cannot know it is narrow before it has been laid out: a container query would need size containment, which stops the panel taking its width from its content. Every row, including one with a custom colour, still takes the layout props of a Flex (direction, horizontal, vertical, gap), which win over stacked. Stacked, the option rows take the whole row and drop a button size, so that they fit a sidebar; whatever still overflows scrolls, and the chosen swatch or option is scrolled into view.

Owning the state

By default the panel reads and writes ThemeProvider, which persists to localStorage. Pass value and onChange to take that over — the panel then writes nothing to the providers or to storage, and you decide what a change means:

changed holds only the keys this interaction touched, which is what you want to send to a server; next is the whole state.

That includes the Theme row: it shows value.theme, reading an unset theme as "system", and reports a choice as { theme } without switching the page.

To keep the panel bound to ThemeProvider but change where choices are stored, set persistence on the provider instead — see ThemeProvider.

Localisation

labels translates every string the panel shows or announces: the group headings, the row labels, the segmented options, the swatches' accessible names and the theme switcher's. Pass it to StylePanel or StylePanel.Root, and every group and row inside reads it, including the ones you compose yourself. Every field at every depth is optional, and whatever you leave out keeps its English default, so you can translate one section at a time.

Key
Covers
groupsEach group's title and description, keyed like visibility: page, color, solidStyle, bodyText, advanced.
rowsEach row's label, keyed by its part name: theme, shape, brand, accent, neutral, solid, effect, surface, scaling, transition, dataStyle, bodySize, bodyLineHeight.
optionsThe segmented options, by row and value (solid.inverse, transition.micro, dataStyle.sequential), and a formatter for each numeric row: scaling, bodySize, bodyLineHeight.
namesDisplay names for shapes, schemes and neutrals. They default to the key ("blue"), and the swatches' names are built from them.
swatchesThe swatches' accessible names, as functions of that display name: shape, scheme (which also gets the row, "brand" or "accent") and neutral. The defaults read Corner style: sharp, Brand colour: blue and Neutral colour: gray.
themeSwitcher"System theme", "Dark theme" and "Light theme". ThemeSwitcher takes the same object as its own labels.
scrollerThe scroll buttons of the colour strips and option rows: "Scroll Previous" and "Scroll Next". Scroller takes the same object as its own labels.
customThe custom colour's swatch, field and hint — see Custom colour.

A row's label and a group's title and description still win over labels, so one composed row can say something else: <StylePanel.Brand label={t("rows.siteColour")} />.

Custom colour

customColors ends the Brand and Accent swatches with one more: a colour of your own, such as a site's brand colour. It is painted with the current seed, and shows a plus while there is none. Choosing it reports { brand: "custom", brandSeed } — the seed already in value, or #5a93fc if there is none — and while it is chosen a colour field under the swatches edits the seed. Every colour picked there comes through onChange, in both next and changed.

For one row only, compose the panel and set custom on that row: <StylePanel.Brand custom />.

It is for the controlled mode. Bound to ThemeProvider, whose setStyle takes no seed, the panel would have nowhere to keep one, so the custom swatch does not render there and the rows look as they always have.

Choosing a named scheme afterwards keeps the seed in the value, so the custom swatch goes on showing it and one click brings it back; the seed only takes effect while the row is "custom". The field's clear button is not reported: the panel keeps the last colour rather than leave the scheme with nothing to point at. The field shows a hex seed; a seed in another format (rgb(), oklch()) still paints the swatch but leaves the field empty until a colour is picked.

labels.custom
Default
swatch(row)The swatch's accessible name: Brand colour: custom (Accent colour: custom).
picker(row)The field's label: Custom brand colour (Custom accent colour).
description(seed, row)None. Text under the field, wired to its aria-describedby.
removeThe field's clear button: Remove.

Rendering the colour

The panel never generates CSS. It reports the seed, and your app renders the scheme from it with the same pure function Once UI uses everywhere. On the published site, give the seed to ThemeInit: it generates the scheme on the server and renders it into a <style> beside its script, so the page paints in the site's colour from the first frame, with no flash. ThemeProvider takes the same seed and applies it on the client.

In the editor, where the colour changes as it is picked, write the same properties yourself with customSchemeProperties:

See Custom schemes for how a scheme is generated from one colour.

API reference

Type
Default
visibility
Which sections and rows to show.
StylePanelVisibility—
value
Take ownership of the state. The panel then writes nothing to the providers or to storage.
StylePanelRootProps["value"]—
onChange
Called with the whole state and just the keys that changed — the latter being what you send to a server.
StylePanelRootProps["onChange"]—
labels
Every string the panel shows or announces, over the English defaults. Groups and rows read it; their own props still win.
StylePanelLabels—
customColors
End the Brand and Accent swatches with a custom colour, reported as brandSeed and accentSeed. Controlled mode only.
boolean—
stacked
Put each row's label above its control, for a narrow sidebar.
boolean—
…and every FlexProps propinherited—
className
string—
style
React.CSSProperties—
Edit this page on GitHub
TrademarkTrademark
Built with curiosity by Lorant One and the Design Engineers Club