Back to catalog
Component detail entry
Utilities
@vean/ui
SThemeCustomizer

ThemeCustomizer

ThemeCustomizer belongs to the Utilities category. Start with demos and usage, then use the API tables to finish integration and styling decisions.

Use the quick jump panel to reach overview, usage, demos, and API first, then continue through the full page.

Quick jump

Related components

If you are composing interactions in the same family, these components usually pair well together.

ThemeCustomizer

Overview

SThemeCustomizer is the visual editing body of the Vean theme engine: it exposes every ThemeOptions field plus per-token light/dark overrides, and writes each change straight to the running theme. Use it when an application wants to ship an in-app theme settings surface.

It is a body, not a shell — it renders no popover, drawer, or sidebar of its own. Host it wherever the application wants: a SPopover behind a settings button, a SDrawer panel, or an inline sidebar region. That split is deliberate: the panel keeps a stable fixed box, and the host decides the container.

It reads and writes the theme context owned by the surrounding SConfigProvider, so it pairs with SThemeModeSegment / SThemeModeSelect when the host also needs a compact light/dark control.

Usage

Features

  • 🧭 Two top-level tabs — Theme (routine settings) and Custom (per-token overrides)
  • 🗂 Eight selectable sections via sections: mode / palette / radius / size / spacing / font / scheme / advanced
  • 🎨 Seven-slot ui map (root / tabs / content / panel / actions) for shell overrides
  • 🧱 A stable box — fixed w-96 h-[70vh] with a scrolling content region and scrollbar-gutter: stable, so switching tabs never resizes the host popover
  • 🌗 Mode handled by SThemeModeSegment, so auto resolves through the OS rather than a binary toggle
  • ✍️ Font editing across four arms (sans / serif / mono / heading), each accepting a preset key or a custom stack
  • 🧬 Custom tab flattens ~41 semantic tokens by group and renders final values, so an override shows the current derived token
  • ♿ Localized labels, size forwarded to every child control, and an axe-clean panel
  • ⚡ Advanced groups mount one animation frame at a time, so opening Custom does not block the first paint

Demos

Basic

API

Aria Layer

1

ThemeCustomizer

7 Props
Props
PropTypeDefaultDescription
class
ClassValue
—
Additional class names applied to the root element.
ui
Partial<ThemeCustomizerUi>
—
Per-slot class overrides for the component shell. `ui.root` carries the outer box, so a host can drop the default fixed width / height (`w-96 h-[70vh]`) when it already constrains the surface itself.
sections
ThemeCustomizerSection[]
—
The sections to show (defaults to all).
size
ThemeSize
—
The size forwarded to the underlying controls.
persist
boolean
true
Whether to persist changes to storage.
showActions
boolean
true
Whether to show the bottom action row (save preset / reset).
labelResolver
((key: string) => string)
—
Resolve a label key (section / group / variant token) to a display string.

Notes

Architecture and benchmark differences

AspectVeanAnt Design / Element Plus / Mantine
EngineOne theme engine (@vean/theme) with a single token contract; the panel edits engine options directlyPer-library theme algorithm or CSS-variable preset; panel is coupled to the library’s own config shape
ContainerSThemeCustomizer is a body only — no popover / drawer / modal wrapperUsually a modal or drawer component with the panel baked in
OverridesTwo layers: ThemeOptions fields plus per-token light/dark overrides with a live final previewA color-picker list or a raw JSON/css-var editor on a separate route
PersistenceSConfigProvider owns the single __VEAN_THEME envelope writer; the panel never writes storage itselfPersistence is left to the application
TokensFlat semantic-token contract; the panel’s groups (surfaces / fills / hairlines / brand / sidebar / feedback / charts) are a presentation vocabulary onlyGroups and tokens are the same list

Cautions

  • Must live inside a SConfigProvider. The panel reads useTheme() and throws (via useTheme('ThemeCustomizer')) when no provider supplies the theme context.
  • persist is currently a no-op. SConfigProvider is the single envelope writer (apply → setThemeState → the derived payload), so the panel does not write __VEAN_THEME itself; persistence follows the provider’s persistTheme setting.
  • No modelValue. The panel is not a controlled form: changes apply to the running theme immediately, and there is no cancel.
  • sections is exhaustive whitelist filtering. Sections you omit are not rendered; the mode section renders only SThemeModeSegment, and advanced covers both the token-override tab and the border-opacity / surface-style rows.
  • The Custom tab is heavy. It mounts ~41 SPalettePickers (each wrapping a full SSelect), so groups are appended one requestAnimationFrame at a time — assert on the mounted state, not on the first frame.
  • ui.root is the box escape hatch. A host that already constrains the surface should override it to drop the default w-96 h-[70vh].
  • The font arms do not load fonts. Selecting a family only writes the CSS variable; loading the font file stays the application’s job.

FAQ

How do I show only a few sections?

Pass sections, e.g. :sections="['mode', 'palette', 'radius']". The array is a whitelist and defaults to all eight sections.

How do I host it in a popover?

Wrap the panel in your own container — the basic example uses SPopover with a settings trigger. If the host already sizes the surface, override ui.root to drop the default fixed width and height.

Where do the changes go?

Into the live theme context of the surrounding SConfigProvider. Each edit commits to the runtime immediately and re-emits the provider’s theme event; persistence is the provider’s job.

How do I get a light/dark control outside the panel?

Use SThemeModeSwitch for a compact toggle or SThemeModeSelect when auto must be selectable. Both bind to the same theme context, so they stay in sync with the panel’s mode section.

How do I add a custom reset?

showActions renders a reset row that calls useThemeSettings().reset. To control the reset target yourself, hide the row (:show-actions="false") and snap the theme back through SConfigProvider / setThemeState.

Why does a token edit show a different value than I typed?

The Custom tab binds each SPalettePicker to the derived token (variants.final), not to the raw override. Once committed, edits stay exact: an override is written verbatim, but a value outside the override list displays the engine’s current derived result.

Changelog

Version history is grouped by release so you can quickly see what changed for this component across published versions.

v0.51.0

2026-10-19

Compare release

Fix

declare the palette select deps and share the locale

Scope: theme-customizerAuthors: soybeanjsView commit5d4b2

v0.50.0

2026-10-02

Compare release

Feature

stabilize shell and remove variable panel

Scope: theme-customizerAuthors: soybeanjsView commit78811

Feature

replace mode dropdown with theme mode segment

Scope: theme-customizerAuthors: soybeanjsView commit49748

Style

simplify event handling and add unit tests for runtime style updates

Scope: theme-customizerAuthors: soybeanjsView commit0a2d3

Feature

replace mode dropdown with theme mode segment

Scope: theme-customizerAuthors: soybeanjsView commit49748

Fix

simplify event handling and add unit tests for runtime style updates

Scope: theme-customizerAuthors: soybeanjsView commit0a2d3

Feature

stabilize shell and remove variable panel

Scope: theme-customizerAuthors: soybeanjsView commit78811

v0.40.0

2026-09-14

Compare release

Docs

switch popover placement to bottom-start

Scope: theme-customizerAuthors: soybeanfeView commitdb819

v0.30.0

2026-08-20

Compare release

Style

change width to 100

Scope: theme-customizerAuthors: soybeanjsView commit7a3a7

Style

change width to 100

Scope: theme-customizerAuthors: soybeanjsView commit7a3a7