Design Tokens
Foundations express design decisions as data. The token layer is a framework-neutral registry that lets any design language — Box's own or a downstream team's — restyle the component catalog without forking component implementations.
Runtime theme switching, boe:design-system-change, and when to use theming vs token authoring are documented in theming.md.
Model
| Layer | Responsibility |
|---|---|
@box/blueprint-web-assets (upstream, not a dependency) | source of Box tokens, icons, and illustrations |
src/foundations/tokens/registry.ts | framework-neutral token + asset registry |
src/foundations/tokens/box-defaults.ts | the Box default design system bundle |
| components | consume registered assets by name and fall back safely when no design system is active |
Important constraint carried over from the original review: the upstream asset package ships React-oriented JS modules. box-open-elements stays framework-agnostic, so the core library must not hard-depend on those React exports at runtime. Treat @box/blueprint-web-assets as an upstream asset source and register the pieces you want into the registry.
API surface
import {
applyDesignTokens,
boxDefaultDesignSystem,
createDesignTokenStyleText,
normalizeDesignTokens,
preloadBoxDefaultIcons,
registerBoxDefaultDesignSystem,
registerDesignSystem,
setActiveDesignSystem,
} from "@unofficialbox/box-open-elements/foundations/tokens";
| API | Purpose |
|---|---|
registerDesignSystem() | register any custom token/icon/illustration bundle |
normalizeDesignTokens() | translate preferred semantic names to internal Box-compatible keys |
registerBoxDefaultDesignSystem() | register the built-in Box light bundle |
registerBoxDarkDesignSystem() | register the built-in Box dark bundle (box-dark) |
setActiveDesignSystem() | switch the active bundle at runtime |
applyDesignTokens() | write token values onto an element as CSS custom properties |
createDesignTokenStyleText() | generate a CSS block for SSR or stylesheet injection |
resolveDesignIcon() / resolveDesignIllustration() | look up a registered asset by name |
preloadBoxDefaultIcons() | load the full named Box icon registry before a synchronous lookup |
Custom bundles should use the typed, lower-camel-case semantic names. The
registry translates them to Box-compatible keys and then applies kebab-cased CSS
custom properties with the --boe-token- prefix:
surfaceBrand → SurfaceSurfaceBrand → --boe-token-surface-surface-brand
The repetitive PascalCase names remain supported for compatibility with Box's
upstream tokens.json inventory and existing consumers, but they are an
internal/conformance vocabulary rather than the preferred authoring API.
Box default example
import {
applyDesignTokens,
registerBoxDefaultDesignSystem,
} from "@unofficialbox/box-open-elements/foundations/tokens";
registerBoxDefaultDesignSystem({ setActive: true });
applyDesignTokens(document.documentElement, "box-default");
Theme registration does not load the full icon registry. The four bespoke icons
(info, alert, folder-shared, file-pdf) resolve immediately. Other named
icons load on first lookup; icon controls refresh when loading finishes. Call
await preloadBoxDefaultIcons() before resolveDesignIcon(name) if synchronous
markup is required. Known icons should use the direct glyph subpath instead.
Dark theme
Status text and glyphs use textStatusSuccess, textStatusWarning, and
textStatusError; status surface colors are decorative fills, not readable ink.
Alert and Toast mix those fills with surfacePrimary, never hardcoded white.
Keep textOnBrand paired with all three brand fill states: the dark bundle uses
dark ink on bright blue, while the light bundle uses white on darker blue.
Custom themes must check normal, hover and pressed text at 4.5:1 or better.
The Button danger tone derives a stronger fill from surfaceStatusError and
textPrimary, leaving the shared status color unchanged. Hosts with a custom
palette may override --boe-button-danger-background on box-button; check
its default, hover, and pressed contrast against textOnBrand at 4.5:1 or
better, including the small size.
box-dark is a built-in bundle with the same token keys, icons, and illustrations as box-default — only the surface / text / stroke / status values change. Because every component reads --boe-token-*, switching the active bundle re-themes the whole catalog with no markup change:
import {
createThemeController,
} from "@unofficialbox/box-open-elements/foundations/theming";
const theme = createThemeController();
theme.start();
theme.setPreference("dark");
The docs-site theme toggles use this controller. Use the registry calls directly only for low-level bundle authoring or token application; see Theming.
Custom design system example
import {
applyDesignTokens,
registerDesignSystem,
} from "@unofficialbox/box-open-elements/foundations/tokens";
registerDesignSystem(
{
name: "acme",
tokens: {
fontFamilyBase: "InterVariable, Inter, sans-serif",
surfacePrimary: "#ffffff",
surfaceBrand: "#5b4bff",
textPrimary: "#111111",
borderDefault: "#e8e8e8",
},
icons: {
search: '<svg viewBox="0 0 16 16"><circle cx="8" cy="8" r="4"></circle></svg>',
},
},
{ setActive: true },
);
applyDesignTokens(document.documentElement, "acme");
Semantic names
| Preferred name | Compatibility key | Purpose |
|---|---|---|
surfacePrimary | SurfaceSurface | primary application/component surface |
surfacePrimaryHover | SurfaceSurfaceHover | hover state for the primary surface |
surfaceSecondary | SurfaceSurfaceSecondary | secondary surface |
surfaceBrand | SurfaceSurfaceBrand | primary brand action/surface |
surfaceBrandHover | SurfaceSurfaceBrandHover | brand hover state |
surfaceBrandPressed | SurfaceSurfaceBrandPressed | brand pressed state |
surfaceItemSelected | SurfaceItemSurfaceSelected | selected collection item |
surfaceItemHover | SurfaceItemSurfaceHover | hovered collection item |
textPrimary | TextText | primary content text |
textSecondary | TextTextSecondary | supporting content text |
textPlaceholder | TextTextPlaceholder | placeholder text |
textOnBrand | TextTextOnBrand | text on brand surfaces |
textDanger | TextTextDanger | destructive/error text |
borderDefault | StrokeStroke | default border/divider |
borderHover | StrokeStrokeHover | emphasized or hovered border |
The exported SemanticDesignTokenMap type includes the full surface, status,
badge, illustration, text, border, and base-font vocabulary. Custom token names
outside that vocabulary remain supported and are converted to --boe-token-*
without translation.
Do not provide both forms of the same token in one bundle. For example,
surfacePrimary and SurfaceSurface together are rejected as ambiguous.
Component consumption rules
- Components consume tokens as CSS custom properties with safe fallbacks:
background: var(--boe-token-surface-surface-brand, #0061d5);
- Components must render sensibly with no design system registered.
- Components that render assets treat asset names as keys into the active design system and fall back to built-in rendering when the key is absent.
Token consumption vs shell / consumer overrides
Source-level Box styling is the default: every catalog surface carries its look in its own shadow styles via --boe-token-*. Demo, docs, and host “shell” CSS must not be treated as the source of the Box look.
| Actor | Responsibility |
|---|---|
| Component author | Paint with --boe-token-* + safe fallbacks inside the shadow tree; expose structural parts for overrides |
| Host / docs shell | Start a theme controller on a root; keep layout chrome separate from component paint |
| App consumer | Theme by swapping design-system bundles; customize structure with ::part(); use rare host custom properties only when the component documents them |
When to use which lever
| Need | Use | Do not |
|---|---|---|
| Brand / theme colors, text, strokes, status | --boe-token-* inside the component (or a registered custom bundle) | Hardcode hex in shadow styles without a token + fallback |
| One-off visual tweak for a single embedding | tag::part(name) { … } from outside | Fork the component or restyle via brittle deep selectors |
| Documented structural host knobs (e.g. collapsed nav label visibility) | Component-owned custom properties such as --boe-nav-label-display | Invent undocumented --boe-* vars on the host |
| Whole-app light/dark or white-label theme | createThemeController with built-in or registered bundle names | Per-page shell CSS that paints over every control |
| Third-party SCSS → token vocabulary | Style bridge (bun run style-bridge — ../integration/style-bridge.md) | Hand-copy third-party rules into shadow trees |
:host { color: inherit; font: inherit; } is intentional so typography can follow the embedding page while paint (background, border, status, brand) still comes from tokens.
Native closed-widget limits
Some native controls only accept limited styling (accent-color, closed UA popups). Prefer tokens for what the platform allows; do not fake a full custom chrome unless the component is upgraded structurally:
<datalist>popups (combobox)- Native
<select>/ date / time picker popups - Bare
<input type="range">tracks (beyondaccent-color)
Docs-site API tab
The component docs API tab lists design tokens used by scanning the primary host’s shadow styles for --boe-token-* references. That inventory is derived from the live preview — empty when a surface truly uses no tokens.
Why this boundary works
| Decision | Reason |
|---|---|
| keep asset package out of core runtime deps | preserves framework neutrality |
| use a registry instead of hard-coded imports | makes design-language swaps explicit and testable |
| keep fallback rendering in components | components still work with no design system registered |
| apply tokens via CSS custom properties | works for both Box defaults and downstream custom themes |
Interactive state helpers
Batch 3 fidelity work ships shared CSS snippets from
@unofficialbox/box-open-elements/foundations/tokens so interactive parts share one
focus/hover/active/disabled language:
import {
boeBrandInteractiveStyles,
boeNeutralInteractiveStyles,
boeFocusVisibleStyles,
} from "@unofficialbox/box-open-elements/foundations/tokens";
const styles = `
${boeNeutralInteractiveStyles('[part="trigger"]')}
${boeBrandInteractiveStyles('[part="confirm"]')}
`;
| Helper | Use when |
|---|---|
boeNeutralInteractiveStyles(selector) | surface buttons, chips, triggers, rows |
boeBrandInteractiveStyles(selector) | primary/confirm actions |
boeFocusVisibleStyles(selector) | controls that already own hover (native inputs) |
boeFocusRingShadow | raw box-shadow value for custom rules |
The focus ring always resolves through --boe-token-surface-surface-brand (opaque brand fallback #0061d5) so it adapts between box-default and box-dark and stays high-contrast when no design system is registered.
Follow-ups
- Expand the Box default bundle only with tokens and assets actually used by components or the docs site.
- Add a build-time extraction script if we want to mirror more of
@box/blueprint-web-assetsautomatically.
Status label contrast
Translucent status labels in Path, RunTrace, Table and Timeline use semantic
TextStatusTextSuccess, TextStatusTextWarning and TextStatusTextError tokens.
Brand and neutral tinted labels use primary text. Keep these text tokens readable
on the themed panel surface when overriding status fills.
Opaque Badge fills keep their tone colors and choose black or white text with
CSS contrast-color() when supported. Older browsers use dark text that covers
the built-in light/dark palettes. A custom opaque palette on those browsers can
set text through box-badge::part(badge); check small labels at 4.5:1 or better.