Design tokens
How Bridge's tokens work, how they're named and read, every token in light and dark, and how to add one.
How a token works
A token is a named color or shadow with a job, like --text-muted or --surface-secondary. It has two layers: the stylesheet gives each token a light and a dark value, and a component reads the token by name. Components never touch the raw value.
Two tokens, from value to use
/* packages/ui/src/styles/globals.css */
:root,
[data-theme="light"] {
--text-muted: #6D7284;
--primary: #0038FF;
}
[data-theme="dark"] {
--text-muted: #868CA3;
--primary: #4D7AFF;
}// What a component writes
<p className="text-[12px] text-[var(--text-muted)]">Intros this year</p>
<div className="h-full rounded-full bg-[var(--primary)]" />data-theme="dark" on <html> swaps every value at once, so the same class is right in both themes. There is no palette underneath (no --blue-500): each value is written straight into the role that uses it, so one role can change without moving another.
Only tokens, in both themes
Switch the preview to Dark. The markup doesn’t change; every color follows.
Intros this year
46
38 accepted, 83%
Why classes say var()
Bridge writes bg-[var(--surface-secondary)] instead of a short class like bg-surface-secondary. The class reads the token on the element itself, so a data-theme="light" area inside a dark page (the handbook’s previews) gets its own values. It is also how Bridge OS has always written tokens, so code moves between the app and the package unchanged.
The package’s Tailwind theme holds only what never changes with the theme: the Mulish font and the entrance and exit animations (animate-fade-in, animate-scale-in and others).
Naming
A token is named for its job, never its color. Tokens that belong together share a start, and the part after it says the variation.
| Pattern | Means | Examples |
|---|---|---|
--surface-* | A background a part sits on. | --surface-primary, --surface-secondary, --surface-elevated |
--border-* | A line around or between parts. | --border-default, --border-hover, --border-divider |
--text-* | Ink for text and icons, by importance. | --text-primary, --text-body, --text-muted |
--{color} | The color itself: icons, dots, bars, links. | --primary, --success, --error, --purple |
--{color}-bg, -border, -text | A soft tint, its outline, and the text that reads on that tint. | --success-bg, --success-border, --success-text |
--{color}-solid | A fill strong enough for white text. Buttons only. | --primary-solid, --danger-solid |
--{color}-hover, -active | The same role when hovered or pressed. | --primary-hover, --primary-solid-hover |
--shadow-* | A whole shadow, named by what floats. | --shadow-menu, --shadow-dialog, --shadow-drawer |
--glass-*, --glow-* | Parts of a utility. Write the utility instead. | liquid-glass, bg-glow-patches |
Colors also have display names from Bridge’s Figma palette, such as Royal, Charcoal and Slate 15. Use them when people talk about colors, in reviews and specs. Code always uses the token: --primary, never a class named after Royal. The reference below shows both.
Look-alikes
| These | Differ by |
|---|---|
--primary and --primary-solid | --primary is for text, icons and tints. --primary-solid is the button fill: white text on it passes in both themes. |
--error, --error-text and --danger-solid | --error is for icons and dots. --error-text is text on a red tint. --danger-solid fills a destructive button. |
--text-body and --text-muted | Body is for reading text. Muted is for metadata: dates, counts, helper text. |
--border-default and --border-divider | Default outlines a part. Divider is the fainter line between rows inside it. |
Token reference
All 64 tokens, read from packages/ui/src/styles/globals.css on every build. Each swatch is drawn on its theme’s own page color, so see-through values look as they do in use.
Surfaces (4)
The page and what sits on it, from the base page to raised menus.
| Token | Light | Dark | Note |
|---|---|---|---|
--surface-primary | #FFFFFFWhite | #0B0F1A | |
--surface-secondary | #F9F9FASlate 05 | #111627 | |
--surface-tertiary | #F2F3F5Slate 10 | #1A2035 | |
--surface-elevated | #FFFFFFWhite | #151B2E |
Borders (3)
Outlines, hover outlines and the faint lines between rows.
| Token | Light | Dark | Note |
|---|---|---|---|
--border-default | #ECEDF0Slate 15 | #252D44 | |
--border-hover | #D9DBE1Slate 30 | #3A4460 | |
--border-divider | #F2F3F5Slate 10 | #1E2640 |
Text (5)
Five steps of ink, from headings to disabled.
| Token | Light | Dark | Note |
|---|---|---|---|
--text-primary | #0D1531Charcoal | #F0F1F4 | |
--text-secondary | #3D445ACharcoal 80 | #C8CCD8 | |
--text-body | #676C7ECharcoal 70 | #9BA1B4 | |
--text-muted | #6D7284 | #868CA3 | 4.55:1 on --surface-secondary (Bridge OS #6E7385 was 4.48), 4.78 on white. Not for --surface-tertiary: use --text-body there. |
--text-disabled | #B3B7C4Slate 60 | #505872 |
Primary (3)
Bridge blue for links, icons, selected states and tints. Not for button fills.
| Token | Light | Dark | Note |
|---|---|---|---|
--primary | #0038FFRoyal | #4D7AFF | |
--primary-hover | #0036D7Royal S10 | #6B91FF | |
--primary-active | #0028C0 | #3A66E8 |
Success (4)
Green: done, accepted, on track.
| Token | Light | Dark | Note |
|---|---|---|---|
--success | #0EA02EKelly | #34C759 | |
--success-bg | #E7F6EAKelly 10 | rgba(52, 199, 89, 0.12) | |
--success-border | #DBF1E0Kelly 15 | rgba(52, 199, 89, 0.2) | |
--success-text | #005F15Kelly S10 | #4ADE80 |
Error (4)
Red: failed, rejected, needs fixing.
| Token | Light | Dark | Note |
|---|---|---|---|
--error | #E13535Ruby | #FF6B6B | |
--error-bg | #FCEBEBRuby 10 | rgba(255, 107, 107, 0.12) | |
--error-border | #F9D7D7Ruby 20 | rgba(255, 107, 107, 0.2) | |
--error-text | #9E0000Ruby S10 | #FFA0A0 |
Button fills (5)
Fills dark enough for white text in both themes. Only buttons use them.
| Token | Light | Dark | Note |
|---|---|---|---|
--primary-solid | #0038FFRoyal | #2F5BFF | primary button fill — white text 4.8:1 |
--primary-solid-hover | #0036D7Royal S10 | #3D68F5 | |
--danger-solid | #C72B2B | #C0392B | destructive button fill — white text 5.5:1 (brand #E13535 = 4.0:1, failed) |
--danger-solid-hover | #A82323 | #D0463A | |
--danger-text | #C72B2B | #F58A8A | danger-ghost text — 5.5:1 on white |
Warning (4)
Amber: waiting, at risk.
| Token | Light | Dark | Note |
|---|---|---|---|
--warning | #F59E0B | #FBBF24 | |
--warning-bg | #FCF4E6Honey 10 | rgba(251, 191, 36, 0.12) | |
--warning-border | #FBEFD9Honey 15 | rgba(251, 191, 36, 0.2) | |
--warning-text | #714A00Honey S10 | #FCD34D |
Purple (3)
An accent for dashboard and feed cards.
| Token | Light | Dark | Note |
|---|---|---|---|
--purple | #552BCBPurple S10 | #9A81ED | |
--purple-bg | #F6F2FFPurple 10 | rgba(139, 110, 232, 0.12) | |
--purple-border | #F1ECFFPurple 15 | rgba(139, 110, 232, 0.2) |
Info (3)
Blue information tints and the blue tag.
| Token | Light | Dark | Note |
|---|---|---|---|
--info | #0036D7Royal S10 | #60A5FA | |
--info-bg | #EEF4FFSky 10 | rgba(96, 165, 250, 0.12) | |
--info-border | #E6EEFFSky 15 | rgba(96, 165, 250, 0.2) |
Honey (1)
The alum sticker’s gold. Only Badge uses it.
| Token | Light | Dark | Note |
|---|---|---|---|
--honey | #E19500Honey | #F5C451 |
Avatar badges (3)
The Super Connector award and the warning mark on an avatar. The same in both themes. Only Avatar uses them.
| Token | Light | Dark | Note |
|---|---|---|---|
--super-connector-from | #6A3AF3 | #6A3AF3 | |
--super-connector-to | #7A80FF | #7A80FF | |
--badge-warning | #B87A00 | #B87A00 |
Navigation (1)
The top bar’s see-through background.
| Token | Light | Dark | Note |
|---|---|---|---|
--nav-bg | rgba(255, 255, 255, 0.8) | rgba(11, 15, 26, 0.85) |
Banner gradient (2)
The two ends of the call-to-action banner.
| Token | Light | Dark | Note |
|---|---|---|---|
--cta-from | #0038FFRoyal | #0D1531Charcoal | |
--cta-to | #0085FF | #151B2E |
Background glows (4)
Read by the bg-glow-patches utility. Use the utility, not these.
| Token | Light | Dark | Note |
|---|---|---|---|
--glow-royal | rgba(0, 56, 255, 0.12) | rgba(77, 122, 255, 0.15) | Primary Royal Blue |
--glow-purple | rgba(85, 43, 203, 0.10) | rgba(139, 110, 232, 0.12) | Purple accent |
--glow-info | rgba(0, 54, 215, 0.08) | rgba(96, 165, 250, 0.10) | Info Blue |
--glow-success | rgba(14, 160, 46, 0.06) | rgba(52, 199, 89, 0.08) | Success Green |
Glass (8)
Read by the liquid-glass utility. Use the utility, not these.
| Token | Light | Dark | Note |
|---|---|---|---|
--glass-bg | rgba(255, 255, 255, 0.9) | rgba(21, 27, 46, 0.9) | |
--glass-bg-strong | rgba(255, 255, 255, 0.9) | rgba(21, 27, 46, 0.9) | |
--glass-bg-nofilter | rgba(255, 255, 255, 0.94) | rgba(21, 27, 46, 0.94) | |
--glass-border | rgba(255, 255, 255, 0.6) | rgba(255, 255, 255, 0.08) | |
--glass-highlight | rgba(255, 255, 255, 0.8) | rgba(255, 255, 255, 0.06) | |
--glass-shadow-outer | rgba(0, 20, 80, 0.06) | rgba(0, 0, 0, 0.3) | |
--glass-shadow-inner | rgba(0, 20, 80, 0.04) | rgba(0, 0, 0, 0.2) | |
--glass-rim | rgba(255, 255, 255, 0.35) | rgba(255, 255, 255, 0.08) |
Shadows (5)
Menus, dialogs and drawers each have their level.
| Token | Light | Dark | Note |
|---|---|---|---|
--shadow-card | 0px 6px 20px rgba(0, 0, 0, 0.15) | 0px 6px 20px rgba(0, 0, 0, 0.5) | |
--shadow-dropdown | 0px 4px 20px rgba(0, 0, 0, 0.12) | 0px 4px 20px rgba(0, 0, 0, 0.6) | |
--shadow-menu | 0px 12px 32px rgba(0, 0, 0, 0.16) | 0px 12px 32px rgba(0, 0, 0, 0.55) | dropdowns, menus, filter popovers |
--shadow-dialog | 0px 24px 64px rgba(0, 0, 0, 0.22) | 0px 24px 64px rgba(0, 0, 0, 0.65) | dialogs, confirms, modals |
--shadow-drawer | -8px 0px 32px rgba(0, 0, 0, 0.12) | -8px 0px 32px rgba(0, 0, 0, 0.5) | side drawers (light falls from the left edge) |
Text selection (2)
Read by the stylesheet’s ::selection rule.
| Token | Light | Dark | Note |
|---|---|---|---|
--selection-bg | rgba(0, 56, 255, 0.2) | rgba(77, 122, 255, 0.3) | |
--selection-text | #0038FFRoyal | #4D7AFF |
Reading tokens in code
| Need | Write |
|---|---|
| A fill, text or line | bg-[var(--surface-secondary)], text-[var(--text-muted)], border-[var(--border-default)] |
| A color at an opacity | bg-[var(--primary)]/8 for an icon square, bg-[var(--surface-secondary)]/60 for a header strip |
| A shadow | shadow-[var(--shadow-menu)], shadow-[var(--shadow-dialog)] |
| Glass, glows, the dot grid | The utility: liquid-glass, bg-glow-patches, bg-dot-grid |
| SVG lines and fills | stroke-[var(--primary)], fill-[var(--surface-primary)], or currentColor with a text class |
| A value in JavaScript (a chart library, a canvas) | getComputedStyle(element).getPropertyValue("--primary"), read again when the theme changes |
Tokens inside an SVG
The line, the baseline and the end dot read the theme’s values, so the chart works in Dark with no extra code.
Adding a token
Rare. Most new colors turn out to be an existing token at an opacity.
- Check whether an existing token does the job. A paler blue is
bg-[var(--primary)]/8; a softer strip isbg-[var(--surface-secondary)]/60; a quieter text is the next text step. - Name it for its job, following the patterns above. Add a light value under
:rootand a dark value under[data-theme="dark"]inpackages/ui/src/styles/globals.css, under the right comment heading. A token without a dark value fails the build. - If it is text or sits behind text, add the pair to the contrast checks (
lib/tokens-core.ts) and make it pass 4.5:1 in both themes. - If its value is a new Figma color, give it its display name in
lib/palette.ts. - Record it in
DESIGN.md→ Color if it adds a new kind of role, and note it in the package’sCHANGELOG.md. - Rebuild. This page reads the stylesheet, so the token shows up in the reference under its heading.
A token in the stylesheet
/* packages/ui/src/styles/globals.css */
:root,
[data-theme="light"] {
/* Text */
--text-muted: #6D7284;
}
[data-theme="dark"] {
/* Text */
--text-muted: #868CA3;
}Don’ts
Hard-coded colors are the most common way a screen breaks in dark mode and drifts from the system in light.
Build from tokens. The same markup is right in both themes.
Write bg-white or text-[#0D1531]. It looks right in light and stays a white slab in dark.
Take metadata from --text-muted and a soft blue tint from --primary at 10%.
Reach for text-gray-500 or bg-blue-50 because it looks close. It’s close in one theme, wrong in the other, and nobody can find it later.
- No hex,
rgb()or Tailwind palette colors (bg-white,text-black,bg-gray-100) in components. - No
dark:patches in app code. If something looks wrong in dark mode, fix the token. - Never fill a button with
--primaryor--error; use the-solidtokens. - Shadows come from
--shadow-*. Never write abox-shadowwith its own color. --shadow-cardand--shadow-dropdownare older shadows, still used by a few tooltips and hover cards. Anything new that floats uses--shadow-menu,--shadow-dialogor--shadow-drawer.
Which color to use for which job, and the contrast of every pair, are on Color. How the theme switch works is on Theming.
Rules
Colors are tokens
Components use role tokens only: bg-[var(--surface-secondary)], text-[var(--text-muted)]. Never hex, rgb() or Tailwind palette classes such as bg-blue-500, and never a dark: patch to fix a color: if dark mode looks wrong, fix the token.