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.

Five hard rules

  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. Never override component internals, the .bb-*__* element classes, from your own code. Restyle through tokens, variants or documented props.
  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 costs the most when you break it, because nothing tells you. Treat everything inside a component as private API.

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.

Three lanes

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

Component look, use props. Pick a variant, a size, compact. Never write CSS to make a component look different.

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.

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 ghost destructive
BbAlertprimary outline destructive warning
BbToast / useToastdefault success info warning destructive
BbConfirm, dropdown items, tooltipsdefault destructive

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
noneUnstyled escape hatch. Avoid

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.

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 rowBbIndicatordefault, success, info, warning, destructive
A persistent messageBbAlertprimary, 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, and there is no --bb-success or --bb-info token. --bb-warn and --bb-text-warn colour the warning input state and nothing else. They are not a variant vocabulary.

A first-class semantic set was considered and rejected. shadcn carries no such vocabulary, and neither does this library.

"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 .bb-button--<name> in a project stylesheet kept apart from the library defaults. The registries are buttonVariants, badgeVariants, alertVariants, confirmVariants, dropdownItemVariants, tooltipVariants and toastVariants. The state recipe is in Design tokens.

The soft treatment to follow, one accent per status:

css
.bb-badge--success {
    --accent: oklch(0.65 0.15 150);
    background: color-mix(in oklab, var(--accent) 12%, var(--bb-panel));
    color: var(--accent);
    border-color: color-mix(in oklab, var(--accent) 25%, transparent);
}

The percentages come from the emphasis ladder, so a variant you register behaves like one the library shipped. 12% sits between the resting 8% and hover 15% stops, and 25% is the tinted-border stop. Mixing toward --bb-panel rather than a hex is what carries it through the dark theme without a second rule.