Branding
You brand BeeUI by changing token values, never token names and never component source.
Every reusable component consumes semantic roles (primary, surface, focus-ring, …), so
re-pointing those roles at your palette re-brands every screen at once.
Prerequisites: a BeeUI app with the Web theme CSS or native token provider already wired (see Start); this guide changes token values, it does not set up theming from scratch — see Theming for that model first if you have not done it yet.
Pick the path that matches how far your brand diverges:
| Path | Use it when | Mechanism | Scope |
|---|---|---|---|
| Runtime overrides | You want your palette on top of a shipped theme | defineThemeOverrides + applyThemeOverrides |
One named runtime theme, changeable at runtime |
| Your own brand themes | You need a complete, named brand (light + dark) registered with the styling engine | defineThemeRegistry + your own theme CSS |
Whole app, or one subtree via BeeThemeScope |
| Source ownership | You want the token CSS itself inside your repository | pnpm beeui add theme |
Your copy of theme.css |
Step 1: keep components semantic
Section titled “Step 1: keep components semantic”Before — the brand color is baked into a reusable component, so it cannot follow a theme, an appearance switch, or a scoped preview:
import { Button, Text } from '@beemvp/beeui-ui';
export function CheckoutButton() { return ( <Button className="bg-[#7c3aed] active:bg-[#6d28d9]"> <Text className="text-white">Pay now</Text> </Button> );}After — the component says what the element is, and the brand lives in exactly one place:
import { Button, Text } from '@beemvp/beeui-ui';
export function CheckoutButton() { return ( <Button> <Text>Pay now</Text> </Button> );}The “after” button now inherits primary, its hover/pressed fills, its foreground and the focus
ring from whichever runtime theme is active — including dark and high contrast — with no further
edits. Step 2 supplies the values.
Theme one subtree instead of the whole app when you need a side-by-side brand comparison, an embedded preview, or a white-label widget:
import { BeeThemeScope, Card, Text } from '@beemvp/beeui-ui';
export function BrandPreview() { return ( <BeeThemeScope brand="violet" appearance="dark"> <Card> <Text>Rendered in the Violet dark runtime theme.</Text> </Card> </BeeThemeScope> );}An unknown brand, appearance, or runtime-theme name throws during render — there is no silent fallback to the wrong brand.
Where className cannot reach (SVG props, chart libraries, navigation theme objects), read the
resolved value instead of re-declaring the color:
import { useBeeToken } from '@beemvp/beeui-ui';
// Web-only illustration: `svg`/`rect` are DOM elements. On native the same hook feeds an// `react-native-svg` element or a chart library prop instead — the hook is the portable part,// the element is not. `react-native-svg` is not a BeeUI peer dependency.export function BrandGlyph() { const fill = useBeeToken('colors.primary'); const radius = useBeeToken('radius.md'); return <svg><rect fill={fill} rx={radius} width={24} height={24} /></svg>;}Step 2: apply your palette at runtime
Section titled “Step 2: apply your palette at runtime”import { applyThemeOverrides, defineThemeOverrides } from '@beemvp/beeui-tokens';import { Uniwind } from 'uniwind';
const acmeLight = defineThemeOverrides({ colors: { primary: '#7c3aed', 'primary-hover': '#6d28d9', 'primary-pressed': '#5b21b6', 'primary-foreground': '#ffffff', 'focus-ring': '#5b21b6', }, radius: { md: 12 },});
applyThemeOverrides(Uniwind, 'light', acmeLight);defineThemeOverrides is pure: it validates keys and value kinds and returns a frozen
{ cssVariables } map. Nothing is applied until the explicit applyThemeOverrides call, and that
call targets exactly one named runtime theme — repeat it for 'dark' (and for every other
runtime theme you ship) if the brand should follow the appearance switch.
radius values compile to px and motion values to ms; color values pass through unchanged
as opaque CSS colors.
Register a brand of your own
Section titled “Register a brand of your own”A theme registry is typed mapping metadata — brand → appearance → runtime-theme name. It is not
a store, a context, or a provider, and constructing one mutates nothing.
import { defineThemeRegistry } from '@beemvp/beeui-tokens';import { Uniwind } from 'uniwind';
const acmeRegistry = defineThemeRegistry({ bee: { light: 'light', dark: 'dark' }, acme: { light: 'acme-light', dark: 'acme-dark' },});
Uniwind.setTheme(acmeRegistry.resolve('acme', 'dark'));defineThemeRegistry only names the mapping — it does not register the theme with the
styling engine. Calling setTheme('acme-dark') before the styling engine knows about
acme-dark throws Uniwind: You're trying to setTheme to 'acme-dark', but it was not registered. at runtime, and tsc rejects 'acme-dark' in a full-project build because the
generated uniwind-types.d.ts only accepts the runtime themes the config below actually
declares. Register every runtime-theme name in your registry as an extraThemes entry in the
Metro/Vite Uniwind config, and give each one a CSS block, before calling setTheme with
it:
// metro.config.js (or the equivalent `uniwind({ ... })` options in vite.config.ts)module.exports = withUniwindConfig(getDefaultConfig(__dirname), { cssEntryFile: './global.css', dtsFile: './uniwind-types.d.ts', extraThemes: ['acme-light', 'acme-dark'],});/* global.css — one @custom-variant + @variant block per extra theme, alongside the base @import lines. Uniwind applies the matching class name for whichever runtime theme is active. */@custom-variant acme-light (&:where(.acme-light, .acme-light *));@custom-variant acme-dark (&:where(.acme-dark, .acme-dark *));
@theme { @variant acme-light { --color-primary: #7c3aed; /* every semantic color role this app relies on, for this theme */ }
@variant acme-dark { --color-primary: #a78bfa; /* the same roles, for the dark appearance */ }}Every brand in one registry must declare the same appearance set, and a runtime-theme name may
not be reused, or construction throws. A registry built this way can also be handed to
BeeThemeScope, which narrows its brand and appearance props to your own vocabulary.
What the runtime supports today
Section titled “What the runtime supports today”Six runtime themes are registered by the shipped token package:
| Runtime theme | Brand | Appearance | Registry |
|---|---|---|---|
light |
Bee | light | beeThemeRegistry |
dark |
Bee | dark | beeThemeRegistry |
violet-light |
Violet | light | beeThemeRegistry |
violet-dark |
Violet | dark | beeThemeRegistry |
high-contrast-light |
Bee | light | beeAccessibilityThemeRegistry |
high-contrast-dark |
Bee | dark | beeAccessibilityThemeRegistry |
High contrast is a second, narrower registry, not an extra appearance on the default one — that keeps Violet from being forced to ship a high-contrast pair it has no evidence for. Apply it through the ordinary theme call:
import { resolveBeeAccessibilityRuntimeTheme } from '@beemvp/beeui-tokens';import { Uniwind } from 'uniwind';
Uniwind.setTheme(resolveBeeAccessibilityRuntimeTheme('bee', 'dark')); // 'high-contrast-dark'There is no automatic “follow the OS high-contrast setting” behavior and no second theme store: selecting the accessibility appearance is an explicit application decision.
What you may customize, and what stays contractual
Section titled “What you may customize, and what stays contractual”Semantic names are the contract. Values are yours; roles are not.
| Token group | Runtime-overridable | Notes |
|---|---|---|
| Semantic colors (34 roles) | Yes | The full colors category of defineThemeOverrides |
radius |
Yes | Numbers, compiled to px |
motionDuration (category key motion) |
Yes | Numbers, compiled to ms |
| Chart colors (10 roles) | No | Theme-defined; readable through the chart.* token paths |
spacing, typography, controlSize, iconSize, avatarSize |
No | Sizing and readability are accessibility surface |
focusRing, elevation, layer, motionEasing |
No | Fixed geometry and stacking contracts |
breakpoint, pageGutter, contentWidth |
No | Build-time responsive constants |
| Private authoring primitives | No | Not a public token group; unreachable by construction |
Anything not flagged runtime-overridable is rejected by defineThemeOverrides with a typed error
rather than silently ignored. Changing an invariant group means changing the canonical token
source and regenerating — a fork-level decision, not a branding knob.
Every registered runtime theme must define the exact same semantic-color vocabulary, so a brand can never add or drop a role for itself alone.
Anti-patterns
Section titled “Anti-patterns”- Hard-coding brand hex in a component. It cannot follow appearance, scope, or an override.
- Renaming or adding semantic roles to express brand nuance. Completeness across all runtime themes is enforced; a one-brand role breaks it.
- Overriding a fill without its paired foreground.
primaryandprimary-foregroundare a contrast pair; moving one and not the other is how brands ship unreadable buttons. - Using
subtle-foreground,disabled,border, oroverlayas brand accents. These carry documented contrast exemptions and are not certified for content. - Treating
classNameas a portable branding API. It is a current-engine escape hatch for application and source-owned code, not a cross-engine promise. - Calling the override once for
'light'and assuming dark follows. Overrides are per named runtime theme.
Known limitations
Section titled “Known limitations”- Contrast is yours to re-verify. BeeUI certifies contrast relationships for its own shipped themes; applying overrides never auto-adjusts a color you supply. If you override a linked pair, re-check it. See Accessibility.
border-strongagainstinput(the unchecked Checkbox/Radio boundary) is certified at 3:1 only for the high-contrast runtime themes; the default themes do not yet meet it. This is a tracked gap, not a decorative exemption.- Overrides are global to a runtime theme, not scoped to a subtree.
BeeThemeScopeselects which named theme a subtree resolves to; it does not scope override values. - The non-hook token read is global-only. Inside a scope, use the
useBeeTokenhook rather than the one-shot snapshot form.
Related
Section titled “Related”- Theming — the semantic token contract end to end.
- Density — the orthogonal layout-density axis.
- CLI & source ownership — copying
theme.cssinto your repo.