Skip to content
Lantern

Tokens

Tokens

Every value the design system is built from, as CSS custom properties on :root. 39 are documented here across 8 groups. Consume them through the Tailwind preset — never by copying values, which is exactly how they drifted between repos before.

Using them

Two imports. The preset supplies the Tailwind theme; the stylesheet supplies the custom properties the theme points at. You need both — the preset alone produces utilities that resolve to nothing.

tailwind.config.ts
// tailwind.config.ts — in any BTS repo
import btsPreset from "bts-storybook/tailwind-preset";

export default {
  presets: [btsPreset],
  content: ["./src/**/*.{ts,tsx}"],
};
main.tsx
// The tokens themselves live in the stylesheet, not the config.
// Import it once, at your application root.
import "bts-storybook/styles.css";

Surfaces

Five levels of background, from the true black the app sits on up to the hover fill. Depth in BTS comes from these steps and a hairline border — not from drop shadows.

--background0 0% 0%bg-background

The page. True black, always.

--background-secondary0 0% 4%bg-background-secondary

Sidebars and the first lift off the page.

--background-tertiary0 0% 10%bg-background-tertiary

Inset wells, code blocks, the tertiary button fill.

--background-hover0 0% 15%bg-background-hover

Hover fill on tertiary surfaces.

--background-input0 0% 10%bg-background-input

Field interiors.

--surface0 0% 7.5%bg-surface

Elevated chrome — modals, headers, toolbars, cards.

--card0 0% 4%bg-card

Card fill.

--popover0 0% 7.5%bg-popover

Floating panels — menus, tooltips, popovers.

Text

Four steps of emphasis. Body copy is `foreground-secondary`; `foreground` is reserved for headings, active states and anything that needs to win.

--foreground0 0% 100%text-foreground

Headings, active items, primary emphasis.

--foreground-secondary0 0% 63%text-foreground-secondary

Body copy and inactive navigation.

--foreground-tertiary0 0% 42%text-foreground-tertiary

Metadata, placeholders, column headers.

--foreground-disabled0 0% 29%text-foreground-disabled

Unavailable text.

Interactive

BTS has no brand hue. Primary is white, and emphasis is carried by the convex fill on `.btn-white` rather than by colour.

--primary0 0% 100%bg-primary

Primary action fill.

--primary-foreground0 0% 0%text-primary-foreground

Text on a primary fill.

--secondary0 0% 10%bg-secondary

Secondary action fill.

--muted0 0% 10%bg-muted

De-emphasised fill.

--muted-foreground0 0% 63%text-muted-foreground

Text on a muted fill.

--accent0 0% 15%bg-accent

Hover fill for ghost and outline actions.

Status

The only saturated colour in the system. Reserve it for meaning — never for decoration.

--success112 100% 55%text-success / bg-success

Positive deltas and confirmations. A deliberately neon green.

--warning38 92% 50%bg-warning

Attention without failure. Also the hue Lantern's own glow is built from.

--destructive0 84% 60%bg-destructive

Deletion and irreversible actions.

Caveat `.btn-destructive` hard-codes hue 0 70% and does not derive from this token — changing the token will not move the button (audit gap #9).

--like0 84% 60%

Reactions.

Caveat Dead. Identical to --destructive, zero references, no Tailwind mapping (audit gap #12).

Borders and ring

Two weights. `border-subtle` is the resting state for cards and rows; `border` is what they move to on hover.

--border0 0% 17%border-border

Hover and active edges, dividers that need to read.

--border-subtle0 0% 12%border-border-subtle

Resting edges on cards, rows and containers.

--input0 0% 17%border-input

Field borders.

--ring0 0% 100%ring-ring

The focus ring. White, offset by two pixels against the background.

Radius

One base value, with Tailwind deriving `sm` and `md` from it.

--radius0.5remrounded-lg

The base. Cards, dialogs, buttons.

--radius-xl1remrounded-xl

Large containers.

--radius-sm0.25rem

Declared but never read.

Caveat Tailwind's `rounded-sm` is calc(var(--radius) - 4px), not this token (audit gap #7).

--radius-lg0.75rem

Declared but never read.

Caveat Contradicts `rounded-lg`, which resolves to 0.5rem via --radius (audit gap #7).

Chart

Five series colours, ordered by priority. Series 1 is white so the primary line always wins.

--chart-10 0% 100%text-chart-1

Primary series.

--chart-2142 71% 45%text-chart-2

Second series.

Caveat Not the same green as --success (112 100% 55%) — a chart will not match its own trend delta (audit gap #11).

--chart-338 92% 50%text-chart-3

Third series.

--chart-40 84% 60%text-chart-4

Fourth series.

--chart-5217 91% 60%text-chart-5

Fifth series.

Custom classes

Not every part of the system is a Tailwind utility. These are hand-written in index.css, and a component that uses one renders unstyled without the stylesheet — so they are part of the contract.

.btn-whiteConvex buttons

The primary button fill — a white gradient with rim light and drop shadow.

.btn-white-pillConvex buttons

The same treatment, fully rounded. Used on logged-out surfaces.

.btn-grey-pillConvex buttons

Secondary pill. The only convex variant with a disabled state.

.btn-destructiveConvex buttons

Destructive primary, same convex treatment in red.

.badge-primaryBadges

Gradient badge fill matching .btn-white. Supplies no radius — the caller sets it.

.nav-itemNavigation

Sidebar item base, plus :hover and .active.

.stat-cardCards

Dashboard metric surface.

.post-cardCards

Feed post surface, with a --static variant that opts out of hover.

.chart-containerCards

Chart frame.

.filter-pillControls

Rounded filter chip, plus .active.

.space-switcher-tileSpace switcher

The tile, its gradient border, active ring and drag states (~25 classes).

.holo-cardEffects

Cursor-tracked holographic shine, glare and border glow.

.shimmerEffects

Loading shimmer. Currently inert — see the keyframe collision below.

.prose-productTypography

Rich-text styling for product descriptions.

.text-linkTypography

Underline-on-hover inline link.

.scrollbar-thinScrollbars

Four-pixel scrollbar for sidebars and compact panels.

Known gaps

Defects the component audit found in the token layer. They are published rather than hidden — a system that conceals its own bugs gets worked around instead of fixed.

P0

#1 @keyframes shimmer is defined twice with different bodies

index.css animates background-position; the Tailwind theme animates transform. Same name, one document — the theme copy lands later and wins, so .shimmer renders as a static gradient. Rename the CSS copy to shimmer-bg.

P0

#3 Inter is not loaded anywhere in the library

The app pulls it in via a <link> in index.html. Storybook has no equivalent, so -apple-system masks the problem on macOS and the wrong typeface ships on Linux CI. Self-host the woff2 or add a preview-head.

P0

#4 darkMode is class-based, but nothing sets the class

Tokens live on :root so colour is fine, but no dark: variant ever activates. ui/alert.tsx already has dark:border-destructive. Set class="dark" on the root element.

P1

#5 theme.screens is not pinned

The whole max-md: / md: contract rides Tailwind's default 768px, but the preset only extends and never fixes screens. A consumer overriding them silently breaks the dual-breakpoint rule.

P1

#8 The --button-white-* tokens are stale

They describe a 73.7% → 100% ramp; .btn-white hard-codes 96% → 64%. Zero references repo-wide. Wire the class to the tokens or delete them.

P1

#9 .btn-destructive does not derive from --destructive

The class uses hue 0 70% throughout; the token is 0 84% 60%. Changing the token will not move the button.

P1

#10 .nav-item.active uses a hard-coded hsl(0 0% 9.5%)

A magic literal, used three times, sitting between --surface (7.5%) and --background-tertiary (10%). It needs a name before the navigation primitive ships.

P2

#19 @tailwindcss/typography is installed but not registered

prose prose-invert is already inert in production. Porting as-is is correct; registering the plugin would make the design system diverge from the app. Decide deliberately.

Where these come from

docs/component-audit.md §2.3 in the bts-storybook repo. That file is the source; this page is a view of it. Fix a gap there and update it here in the same change.