busy-office-ui
Menu

Theming guide

Re-skin the whole framework by remapping the semantic token tier in your own unlayered stylesheet — the cascade contract guarantees it wins. Never override component classes; never touch the raw palette tier. Ready-made palettes ship — see the Palettes reference.

The contract

  • Remap semantic tokens only (--bo-color-*, fonts, radii). Component-local tokens (--bo-btn-*) are for per-instance tweaks, not brand.
  • Keep the token roles honest: *-solid carries white text in both themes (≥ 4.5:1); *-text sits on *-subtle (≥ 4.5:1); dark mode remaps *-text/*-subtle, never *-solid.
  • Verify contrast when you swap hues — the roles encode the ratios, your values must honor them.
  • The dark rule: unlayered :root beats the framework's layered dark remaps — every token you set must be re-set in your own [data-theme="dark"] block if dark differs.
  • An accent swap is a family swap: accent, accent-strong, accent-solid, accent-subtle, accent-text, plus focus-ring and bg-selected — miss one and the UI is two-toned.

brand.css

/* brand.css — imported AFTER the framework, outside any @layer,
   so it wins by the cascade contract. Re-skin by remapping the SEMANTIC
   tier only — never touch component files.

   THE DARK RULE: an unlayered :root declaration beats the framework's
   LAYERED [data-theme="dark"] remap of the same token. So every token you
   set on :root must be re-set in your own dark block if dark differs —
   otherwise your light value leaks into dark mode. */
:root {
  --bo-color-accent: #0f766e;        /* your brand teal */
  --bo-color-accent-strong: #115e59;
  --bo-color-accent-solid: #0f766e;  /* must keep >= 4.5:1 under white text */
  --bo-color-accent-subtle: #f0fdfa;
  --bo-color-accent-text: #115e59;   /* >= 4.5:1 on accent-subtle */
  /* A full accent swap also includes the accent-adjacent tokens the
     framework colors blue by default: */
  --bo-color-focus-ring: #0d9488;    /* else focus rings stay blue */
  --bo-color-bg-selected: #f0fdfa;   /* else selected rows stay blue */
  --bo-font-sans: "Your Corporate Sans", system-ui, sans-serif;
  --bo-radius-md: 0.25rem;           /* sharper corporate corners */
}

[data-theme="dark"] {
  --bo-color-accent: #14b8a6;        /* REQUIRED: re-set every :root token */
  --bo-color-accent-text: #5eead4;   /* light step for dark surfaces */
  --bo-color-accent-subtle: #134e4a;
  --bo-color-bg-selected: #134e4a;
}

A real, shipped preset — @busy-office/ui/css/brand-indigo

Six presets ship, each aimed at a real ERP brand profile: graphite (monochrome), cobalt (enterprise blue), navy (deep conservative blue), forest (green), indigo, and violet — browse and compare them on the Palettes page. Indigo below is one of them: not just this page's prose example — an actual importable file (packages/core/src/css/brand/brand-indigo.css), validated by the same contrast gate as the base theme (scripts/check-contrast.mjs now checks every file in src/css/brand/, not asserted by hand). The live preview below applies the identical token values scoped to just this box (for a real app, they'd go on :root as shown above — this page keeps its own default teal everywhere else on purpose):

Switching themes at runtime

Flip data-theme on the root. One catch: a component's background-color transition animates on the switch while the text color snaps instantly, so text can flash on a still-light background (near-invisible going to dark). Freeze transitions for the one frame of the flip with data-bo-theming (shipped in the utilities layer):

function setTheme(value) {
  const html = document.documentElement;
  html.setAttribute('data-bo-theming', '');   // freeze transitions
  html.dataset.theme = value;
  requestAnimationFrame(() =>
    requestAnimationFrame(() => html.removeAttribute('data-bo-theming')));
}

Two frames: one to paint the new theme with transitions off, the next to re-enable them. Hover/focus transitions are unaffected.

Versioning policy

Semantic tokens, documented class names, and data-*/ARIA contracts are the public API — breaking any of them is a major version. Component-internal selectors and the raw palette are not API. Per-component dist file placement is explicitly not API until v1.0 — granular importers should pin versions; moves are recorded in the CHANGELOG. New components/tokens are minors; visual-only tweaks within the same contracts are patches.

↑↓ move Enter open Esc close