Skip to content

Theming

@bquery/ui uses CSS custom properties for all design tokens. You can override any token at any scope.

Tokens come in two layers, and knowing which one you are reaching for saves a lot of guesswork:

  • Primitives — the raw palette and scales: --bq-color-primary-600, --bq-space-4, --bq-radius-lg. These are the same in light and dark.
  • Semantics — what a primitive is being used for: --bq-bg-base, --bq-text-muted, --bq-surface-overlay, --bq-focus-ring. These change with the colour scheme.

Retheme with the primitives; reach for the semantics when you want a surface or a piece of text to behave differently from the rest.

Global token override

css
:root {
  --bq-color-primary-600: #7c3aed;   /* purple primary */
  --bq-color-primary-700: #6d28d9;
  --bq-radius-lg: 0.75rem;           /* larger border radius */
  --bq-font-family-sans: 'Roboto', sans-serif;
}

Scoped override

Custom properties inherit through shadow boundaries, so scoping a theme to part of the page needs nothing special:

css
.checkout {
  --bq-color-primary-600: #059669;
}

Per-component override

Every component also honours tokens set on the element itself:

html
<bq-button style="--bq-radius-lg: 9999px">Pill</bq-button>

Using applyThemeTokens

typescript
import { applyThemeTokens } from '@bquery/ui/theme';

applyThemeTokens({
  '--bq-color-primary-600': '#7c3aed',
  '--bq-color-primary-700': '#6d28d9',
});

// Or scoped to one element:
applyThemeTokens({ '--bq-radius-lg': '1rem' }, document.querySelector('.card-grid'));

Values set this way land in the element's inline style, so they outrank the theme in both schemes. To override only one scheme, write a rule against [data-theme="dark"] instead — see Dark Mode.

Semantic tokens

TokenUsed for
--bq-bg-baseThe page-level background a component sits on
--bq-bg-subtleQuiet fills: table headers, inset panels
--bq-bg-mutedStronger fills: disabled controls, hovered rows
--bq-bg-hoverTranslucent hover wash; tints whatever is beneath it
--bq-bg-activeTranslucent pressed wash
--bq-bg-selectedSelected rows and options
--bq-surface-raisedCards — above the page
--bq-surface-overlayMenus, popovers, dialogs, toasts — above everything
--bq-surface-sunkenWells and recessed areas
--bq-text-baseBody text
--bq-text-mutedSecondary text, hints
--bq-text-subtlePlaceholders, disabled text
--bq-text-inverseText on --bq-bg-inverse
--bq-border-baseDividers and quiet outlines
--bq-border-emphasisControl borders
--bq-border-strongBorders that need to read at a glance
--bq-border-focusThe border colour a focused control takes
--bq-focus-ringThe focus ring box-shadow
--bq-overlay-backdropDialog and drawer scrims

Intent surfaces

A tinted status surface — an alert, a badge, a chip, a tag — needs a pair of colours that swap together with the scheme. Reaching into the palette for --bq-color-success-100 on --bq-color-success-700 hard-codes a light treatment: those steps are near-white and near-black no matter what the page is doing, so the badge stays glaring white on a dark page.

TokenUsed for
--bq-intent-{intent}-bgThe tinted panel or pill
--bq-intent-{intent}-fgText and icons on that tint
--bq-intent-{intent}-borderIts outline

{intent} is one of primary, success, danger, warning, info, neutral. Use these anywhere you would otherwise reach for a 50/100/200 step as a background.

The surfaces are a stack — sunken, base, raised, overlay. In light mode they are told apart by shadow; in dark mode shadows are invisible, so each level is a slightly different lightness. That is why an overlay has its own token rather than reusing --bq-bg-base.

Primitive tokens

Colors

Each of primary, secondary, danger, success, warning and info ships a full 50–900 ramp.

TokenDefault
--bq-color-primary-600#2563eb
--bq-color-danger-600#dc2626
--bq-color-success-600#16a34a
--bq-color-warning-600#d97706

Typography

TokenDefault
--bq-font-family-sans'Inter', system-ui, ...
--bq-font-family-mono'JetBrains Mono', ...
--bq-font-size-md1rem
--bq-font-weight-semibold600
--bq-line-height-normal1.5
--bq-letter-spacing-wide0.04em

Spacing & radius

TokenDefault
--bq-space-41rem
--bq-radius-lg0.5rem
--bq-radius-full9999px

Control metrics

One height scale for everything that sits on a form row, so a button, an input and a select line up on the same baseline.

TokenDefault
--bq-control-height-sm2rem
--bq-control-height-md2.5rem
--bq-control-height-lg3rem
--bq-control-padding-md0.875rem
--bq-border-width-control1.5px

Elevation

TokenUsed for
--bq-shadow-xsButtons at rest
--bq-shadow-smCards
--bq-shadow-mdRaised panels
--bq-shadow-lgMenus, popovers, toasts
--bq-shadow-xlDialogs, drawers

The dark theme replaces these with much stronger values: a shadow tuned for white paper is invisible on a dark surface.

Motion

TokenDefault
--bq-duration-instant75ms
--bq-duration-fast150ms
--bq-duration-normal200ms
--bq-easing-standardcubic-bezier(0.4, 0, 0.2, 1)
--bq-easing-emphasizedcubic-bezier(0.2, 0, 0, 1)

Every component honours prefers-reduced-motion: reduce by collapsing durations, so you do not have to opt out per component.

Released under the MIT License.