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
- Never hardcode a colour in the CSS around the components. Use the
--bb-*properties and your surfaces follow the theme and dark mode. - 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. - One primary action per view region. Everything else is
secondary,outlineorghost. - Keep
destructivefor irreversible actions, and send a destructive flow throughuseConfirmfirst. - 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
ptparts, plus a component's root class and its documented modifiers (.bb-badge,.bb-button--destructive). npx bitboss-ui checkreads 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--coloron.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.
| Family | Built-in variants |
|---|---|
BbButton / BbDropdownButton | primary secondary outline ghost destructive link, plus none |
BbBadge | primary secondary outline destructive, plus none |
BbAlert | primary outline destructive warning |
BbIndicator | default success info warning destructive |
BbToast / useToast | default success info warning destructive |
BbConfirm, dropdown items, tooltips | default destructive |
Text-like form controls: BbTextInput, BbSelect and the rest | outline secondary ghost, plus none |
BbTabs | pill 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
BbIndicator
BbAlert
Text-like form controls
BbTabs
<template>
<div class="grid gap-8">
<section class="grid gap-3">
<h3 class="m-0 text-sm font-medium opacity-70">BbButton</h3>
<div class="flex flex-wrap items-center gap-2">
<BbButton v-for="variant in buttonVariants" :key="variant" :variant="variant">{{ variant }}</BbButton>
</div>
</section>
<section class="grid gap-3">
<h3 class="m-0 text-sm font-medium opacity-70">BbBadge</h3>
<div class="flex flex-wrap items-center gap-2">
<BbBadge v-for="variant in badgeVariants" :key="variant" :variant="variant">{{ variant }}</BbBadge>
</div>
</section>
<section class="grid gap-3">
<h3 class="m-0 text-sm font-medium opacity-70">BbIndicator</h3>
<div class="flex flex-wrap items-center gap-6">
<BbIndicator v-for="variant in indicatorVariants" :key="variant" :text="3" :variant="variant">
<BbButton prepend:icon="lucide:bell" variant="outline">{{ variant }}</BbButton>
</BbIndicator>
</div>
</section>
<section class="grid gap-3">
<h3 class="m-0 text-sm font-medium opacity-70">BbAlert</h3>
<div class="grid gap-3 md:grid-cols-2">
<BbAlert
v-for="alert in alerts"
:key="alert.variant"
hide-close
:icon="alert.icon"
:text="alert.text"
:title="alert.variant"
:variant="alert.variant"
/>
</div>
</section>
<section class="grid gap-3">
<h3 class="m-0 text-sm font-medium opacity-70">Text-like form controls</h3>
<div class="grid gap-3 sm:grid-cols-3">
<BbTextInput
v-for="variant in inputVariants"
:id="`gallery-input-${variant}`"
:key="variant"
v-model="searches[variant]"
:label="variant"
:name="`gallery-input-${variant}`"
placeholder="Search orders"
prepend:icon="lucide:search"
:variant="variant"
/>
</div>
</section>
<section class="grid gap-3">
<h3 class="m-0 text-sm font-medium opacity-70">BbTabs</h3>
<div class="grid gap-4 sm:grid-cols-2">
<BbTabs v-model="pillTab" :items="tabs" variant="pill" />
<BbTabs v-model="lineTab" :items="tabs" variant="line" />
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import {
BbAlert,
BbBadge,
BbButton,
BbIndicator,
BbTabs,
BbTextInput,
type AlertVariantType,
type BadgeVariantType,
type ButtonVariantType,
type IconType,
type IndicatorVariantType,
type InputVariantType,
} from 'bitboss-ui';
// The built-in names only. `none` is left out: it draws nothing until you style it.
const buttonVariants: ButtonVariantType[] = ['primary', 'secondary', 'outline', 'ghost', 'destructive', 'link'];
const badgeVariants: BadgeVariantType[] = ['primary', 'secondary', 'outline', 'destructive'];
const indicatorVariants: IndicatorVariantType[] = ['default', 'success', 'info', 'warning', 'destructive'];
const inputVariants: InputVariantType[] = ['outline', 'secondary', 'ghost'];
const alerts: { variant: AlertVariantType; icon: IconType; text: string }[] = [
{ variant: 'outline', icon: 'lucide:info', text: 'Prices exclude VAT until checkout.' },
{ variant: 'primary', icon: 'lucide:rocket', text: 'Stock now syncs every 15 minutes.' },
{ variant: 'warning', icon: 'lucide:triangle-alert', text: 'Two products are low on stock.' },
{ variant: 'destructive', icon: 'lucide:circle-x', text: 'The import stopped at row 42.' },
];
const searches = reactive<Record<string, string>>({ outline: '', secondary: '', ghost: '' });
const tabs = [
{ value: 'overview', label: 'Overview' },
{ value: 'activity', label: 'Activity' },
{ value: 'members', label: 'Members' },
];
const pillTab = ref('overview');
const lineTab = ref('overview');
</script>
What the shared names mean:
| Variant | Use for |
|---|---|
primary | The single main action of a view region |
secondary | Supporting actions |
outline | Neutral actions on busy surfaces |
ghost | Low-emphasis, inline or icon-only actions |
destructive | Irreversible actions, confirm first |
link | Navigation disguised as text; buttons only |
none | A 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:
- The brand colour is wrong (primary should be blue, not black). Change the
--bb-primaryknob in your theme. Every surface, derived value and dark-mode variant follows. This is the theme builder's path. - One area should differ (a toolbar, a marketing header). Re-point the same
knobs on a
bb-themeisland, as in Writing a theme. Rings, hovers and radii re-derive for that area. - 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-pressedand--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-panelkeeps 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.
| Need | Reach for |
|---|---|
| A status marker next to a label or row | BbIndicator — default, success, info, warning, destructive |
| A persistent message | BbAlert — primary, outline, warning, destructive |
| Transient feedback | BbToast and useToast — the full status set |
| A removable or error tag | BbBadge destructive; neutral tags use secondary or outline |
| A field that needs attention but is not invalid | any 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 BbDropdownButtonbadgeVariants: class.bb-badge--<name>, for BbBadgealertVariants: class.bb-alert--<name>, for BbAlertindicatorVariants: class.bb-indicator--<name>, for BbIndicatortoastVariants: class.bb-toast__panel--<name>, for useToastconfirmVariants: class.bb-confirm--<name>, for useConfirmdropdownItemVariants: class.bb-dropdown__item--variant-<name>, for dropdown itemstooltipVariants: class.bb-tooltip--<name>, for tooltipsinputVariants: class.bb-field-input--<name>, for text-like fieldstabsVariants: 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
<template>
<ul class="m-0 grid list-none gap-0 p-0">
<li
v-for="order in orders"
:key="order.ref"
class="flex items-center justify-between gap-3 border-b border-[var(--bb-border)] py-2.5 last:border-b-0"
>
<span class="grid min-w-0 gap-0.5">
<span class="text-sm font-medium">{{ order.ref }}</span>
<span class="truncate text-xs text-[var(--bb-text-muted)]">{{ order.customer }}</span>
</span>
<!-- A preview without registering: `none` plus a class. In an app, register the names and use variant="soft-blue". -->
<BbBadge :class="`soft soft-${order.tone}`" variant="none">{{ order.status }}</BbBadge>
</li>
</ul>
</template>
<script setup lang="ts">
import { BbBadge } from 'bitboss-ui';
const orders = [
{ ref: 'SO-2041', customer: 'Harbour Coffee Roasters', status: 'Draft', tone: 'gray' },
{ ref: 'SO-2040', customer: 'Nordlicht Studio', status: 'Awaiting payment', tone: 'blue' },
{ ref: 'SO-2038', customer: 'Verde Market', status: 'Delivered', tone: 'green' },
{ ref: 'SO-2037', customer: 'Atelier Rossi', status: 'Address to check', tone: 'amber' },
{ ref: 'SO-2035', customer: 'Kestrel Outdoor', status: 'Payment failed', tone: 'red' },
];
</script>
<style scoped>
.soft-gray {
--accent: var(--bb-text-muted);
}
.soft-blue {
--accent: var(--bb-info);
}
.soft-green {
--accent: var(--bb-success);
}
.soft-amber {
--accent: var(--bb-warn);
}
.soft-red {
--accent: var(--bb-danger);
}
.soft {
--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);
}
</style>
Register the names:
// 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:
.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.