Skip to content

Design language

Style the screen around the components so what you build looks like part of the same product.

On this page

This page is about the CSS you write next to the components. Design tokens is the architecture underneath it. This is the part you apply while building a screen. If you do not have a theme block yet, generate one in the theme builder before styling the page around it.

Rules for CSS next to the components

  1. Never hardcode a colour in the CSS around the components. Use the --bb-* properties and your surfaces follow the theme and dark mode.
  2. Treat an override on an inner class as a last resort. Reach for a variant, then a token, then pt, and only then CSS on a .bb-*__* class.
  3. One primary action per view region. Everything else is secondary, outline or ghost.
  4. Keep destructive for irreversible actions, and send a destructive flow through useConfirm first.
  5. Respect the radius. Use var(--bb-radius) for any container of your own that sits next to a component. Do not invent corner radii.

Rule 2 is about what survives an upgrade. An inner class such as .bb-table-header__resizer may change in any release without a rename table, so an override on it is yours to maintain. Keep those in one stylesheet, each with a comment saying why pt was not enough. A :deep() rule in a scoped style counts too; Migrating a :deep() rule shows how to move one to pt.

  • Semver covers tokens, variants, props and pt parts, plus a component's root class and its documented modifiers (.bb-badge, .bb-button--destructive).
  • npx bitboss-ui check reads your stylesheets too. It flags a selector on a .bb-* class the library does not render and names the real one. It also flags a token nothing reads, such as a retired --bb-* name or --color on .bb-badge. After an upgrade, npx bitboss-ui changelog --since <version> lists what moved.

Some components are built to be extended visually, and each one names its extension point on its own page. BbTree has you draw connector lines anchored to its --indent and --gap. BbTabs exposes --list-h. Read the component's page before you write any CSS that mentions a .bb- class.

Four lanes

In order of preference. Most work never leaves the first two.

Component look, use props. Pick a variant, a size, compact. Do not write CSS to make a component look like something a variant already does.

Page layout and spacing, use utilities on your own wrapper elements: flex items-end gap-3, grid gap-6, max-w-3xl. Components size themselves. You only place them. On a touch screen the small controls already grow their tap area to 44px: close buttons, group rows, rating stars and colour swatches. Rows and stars spread a little to fit. Do not pad them yourself.

A utility class on a component that does nothing, such as px-6 on a BbButton, is a layer problem, not a token problem. Tailwind v4 needs the cssLayers plugin option; see Installation.

Retuning one component, set its own tokens. Every component declares short --* tokens: --cell-h on .bb-table, --indent on .bb-tree. Set them on the element that declares them, for one instance or a whole scope. Overriding a component token has the rules. A missing --bb-table-row-h is this lane telling you where to look, not a reason for an inner-class override.

Genuinely custom visuals, use --bb-* tokens. The few rules left over are things like a panel of your own or a status dot. Build them from theme tokens and they follow theming and dark mode with nothing extra written.

The variant vocabulary

Variant names are typed per family, so an unregistered name is a compile error rather than an unstyled element.

FamilyBuilt-in variants
BbButton / BbDropdownButtonprimary secondary outline ghost destructive link, plus none
BbBadgeprimary secondary outline destructive, plus none
BbAlertprimary outline destructive warning
BbIndicatordefault success info warning destructive
BbToast / useToastdefault success info warning destructive
BbConfirm, dropdown items, tooltipsdefault destructive
Text-like form controls: BbTextInput, BbSelect and the restoutline secondary ghost, plus none
BbTabspill line none

The gallery renders every built-in name live, except none, which draws nothing until you style it. Switch the site to dark mode to see them follow the theme. Toasts, confirms, dropdown items and tooltips live behind an interaction, so their own pages show them.

BbButton

BbBadge

primarysecondaryoutlinedestructive

BbIndicator

33333

BbAlert

outline
Prices exclude VAT until checkout.
primary
Stock now syncs every 15 minutes.

Text-like form controls

BbTabs

What the shared names mean:

VariantUse for
primaryThe single main action of a view region
secondarySupporting actions
outlineNeutral actions on busy surfaces
ghostLow-emphasis, inline or icon-only actions
destructiveIrreversible actions, confirm first
linkNavigation disguised as text; buttons only
noneA blank start for a one-off look (BbButton shows one). When it recurs, register a variant

The sets differ per family on purpose. BbToast carries success and info because transient feedback is where they belong. BbButton and BbBadge do not, which is what the last section is about.

Changing a built-in variant

A variant is not one declaration. primary on a button sets --bg, --bg-hover, --bg-pressed, --fg, --border-color and --ring-color. Choose how far the change should reach, and start at the top:

  1. The brand colour is wrong (primary should be blue, not black). Change the --bb-primary knob in your theme. Every surface, derived value and dark-mode variant follows. This is the theme builder's path.
  2. One area should differ (a toolbar, a marketing header). Re-point the same knobs on a bb-theme island, as in Writing a theme. Rings, hovers and radii re-derive for that area.
  3. One component's take on one variant is wrong (badges too loud, buttons fine). Re-point that family's local tokens, such as .bb-button--primary { --bg: … }. Rest, hover and press read the same locals, so all three follow.

Two quiet failures to avoid. Setting background-color alone leaves the old brand in hover and focus, which only shows on interaction. Replacing a value derived with color-mix() hardcodes it for both schemes, and "dark mode looks wrong" surfaces months later. Override the knob instead.

Using colour

Reach for colour more than the monochrome default suggests, and make it mean something. A calm, mostly neutral surface with a few deliberate spots of colour reads as confident. Colour sprinkled for decoration reads as noise.

Add colour for one of three reasons, never for prettiness:

  • Semantic status. A build passed, a payment failed, a quota is low.
  • State. Selected, active, dirty, pending, just changed. Colour is a fast, pre-verbal "this updated".
  • Telling apart a small, closed set. A few statuses or priorities the eye should separate at a glance. Keep the set small and fixed; a per-row rainbow is noise, not signal.

Then keep it soft:

  • Leave the chrome neutral. Hover, pressed and focus use --bb-hover, --bb-pressed and --bb-ring, and selection uses the primary tint. Put semantic colour on the content: a status dot, a soft badge, a tinted alert. That is what keeps colour from feeling jarring.
  • Prefer soft tints to saturated fills on anything bigger than a dot. The library's own recipe is color-mix(in oklab, <hue> 10–15%, var(--bb-panel)) for the fill, with the hue itself as the text. Mixing toward --bb-panel keeps it theme- and dark-mode-aware. Small dots may be saturated.
  • One primary action per region still holds. Semantic colour is not a licence for five loud buttons.

What already carries status

Reach for these before you build anything.

NeedReach for
A status marker next to a label or rowBbIndicator — default, success, info, warning, destructive
A persistent messageBbAlert — primary, outline, warning, destructive
Transient feedbackBbToast and useToast — the full status set
A removable or error tagBbBadge destructive; neutral tags use secondary or outline
A field that needs attention but is not invalidany form control's warnings and hasWarnings props

The warning input state has no equivalent elsewhere, so read that last row twice. Every form control takes warnings, the messages, and hasWarnings, which forces the chrome. They mirror errors and hasErrors exactly, and while the messages show they are referenced from the control's aria-describedby. Set both and the errors win: the warnings never appear.

Registering your own

BbButton and BbBadge ship no success, info or warning variants. The status tokens exist for the library's own states: --bb-success and --bb-info fill the toast and the indicator, and --bb-warn and --bb-text-warn colour the warning input state. They are not a variant vocabulary, but your own variants can mix from them.

A first-class semantic set was considered and rejected: shadcn carries no such vocabulary, and neither does this library. The same goes for meters, ramps and gauges. Their hues are project CSS, mixed toward theme tokens with color-mix() so they stay dark-mode aware, and the library will not ship a positive-end token.

"Locked" means typed, not closed. The registries are open by design, so each app owns its own tones. Register the name through the Vite or Nuxt plugin, then style its class in a project stylesheet kept apart from the library defaults. A registered name adds to the built-in ones.

  • buttonVariants: class .bb-button--<name>, for BbButton and BbDropdownButton
  • badgeVariants: class .bb-badge--<name>, for BbBadge
  • alertVariants: class .bb-alert--<name>, for BbAlert
  • indicatorVariants: class .bb-indicator--<name>, for BbIndicator
  • toastVariants: class .bb-toast__panel--<name>, for useToast
  • confirmVariants: class .bb-confirm--<name>, for useConfirm
  • dropdownItemVariants: class .bb-dropdown__item--variant-<name>, for dropdown items
  • tooltipVariants: class .bb-tooltip--<name>, for tooltips
  • inputVariants: class .bb-field-input--<name>, for text-like fields
  • tabsVariants: class .bb-tabs-list--<name>, for BbTabs

Input and tab variants take a block of tokens, not rules. For a field, set --bg, --bg-hover, --bg-disabled, --border-color and --border-focus. For a tab strip, set --list-bg, --indicator-bg and --active-fg; BbTabs lists the rest. The state recipe is in Design tokens.

The soft status palette is the one to start from. Here it is on an order list:

  • SO-2041Harbour Coffee RoastersDraft
  • SO-2040Nordlicht StudioAwaiting payment
  • SO-2038Verde MarketDelivered
  • SO-2037Atelier RossiAddress to check
  • SO-2035Kestrel OutdoorPayment failed

Register the names:

ts
// vite.config.ts (Nuxt: the same array under the `bitboss` key)
bitbossUi({
    badgeVariants: [
        'soft-gray',
        'soft-blue',
        'soft-green',
        'soft-amber',
        'soft-red',
    ],
});

Then add one block to your project stylesheet. Each variant takes its accent from a theme token, so a theme that retunes --bb-success retunes the badge too:

css
.bb-badge.bb-badge--soft-gray {
    --accent: var(--bb-text-muted);
}
.bb-badge.bb-badge--soft-blue {
    --accent: var(--bb-info);
}
.bb-badge.bb-badge--soft-green {
    --accent: var(--bb-success);
}
.bb-badge.bb-badge--soft-amber {
    --accent: var(--bb-warn);
}
.bb-badge.bb-badge--soft-red {
    --accent: var(--bb-danger);
}

.bb-badge:is(
    .bb-badge--soft-gray,
    .bb-badge--soft-blue,
    .bb-badge--soft-green,
    .bb-badge--soft-amber,
    .bb-badge--soft-red
) {
    --bg: color-mix(
        in oklab, var(--accent) 15%, var(--bb-panel)
    );
    --fg: color-mix(
        in oklab, var(--accent) 45%, var(--bb-text)
    );
    --bw: 1px;
    --border-color: color-mix(
        in oklab, var(--accent) 25%, var(--bb-panel)
    );
    --ring-color: color-mix(
        in oklab, var(--accent) 45%, transparent
    );
}

The badge paints from --bg, --fg and --border-color, so set those rather than background and color. The text mix stops at 45% because every accent clears 4.5:1 on its 15% tint there, in both schemes. At 60% blue and green fall to 4.4:1 in dark, and at 80% to about 3:1. Mixing toward --bb-panel and --bb-text rather than a hex carries the variant through dark mode without a second rule.