Agents
- Free, source available
- Local: no account, no backend
- Your repo, your Claude, your GitHub token
- Any DTCG token repo
llms.txt
One source, two outputs. The pages you read and the file an agent reads come from the same build, so they cannot disagree. /llms.txt holds the agent rules from AGENTS.md, every semantic token with its light and dark value, the scales, failing contrast pairs, and each component's props and usage rules.
- lines
- 2202
- agent rules
- 18
- semantic tokens
- 119
- components
- 53
- KB of plain text
- 176
Agents follow short, structured context better than long prose style guides. Values come from the token files, not from a copy.
The hard rules are not only in this text: CI blocks raw colors and shadows, so an agent that ignores the file still cannot merge.
Preview llms.txt (2202 lines)
# Bridge design system
> A design system run as code: W3C DTCG tokens (primitives, brand roles, light/dark semantic aliases, density and
> component tiers) and 53 React components that read CSS variables only. The repo is the source of truth, not Figma.
Generated from the repository on 2026-11-11. Source: https://github.com/the-bridge-company/ai-design-system-demo
Human version of the same content: the docs site pages /foundations/color/ (and the other foundation pages listed below), /tokens/, /components/, /agents/ and /how-review-works/ (Library, home at /library/).
What Bridge does, for people: / (overview) and /compare/ (Bridge next to other design system tools).
## Machine-readable files
- [/tokens.css](/tokens.css): every token as a CSS variable, built from this checkout (the file components read)
- /docs-ai/<component>.json: the facts from the code a component's docs are drafted from (props, tokens, anatomy, contrast), for example [/docs-ai/icon.json](/docs-ai/icon.json)
## Library pages (read, latest release)
- [Home](/library/): What's new and where to start
- [Color](/foundations/color/): Palette ramps and the semantic roles in both themes
- [Typography](/foundations/typography/): Type roles, the scale and samples
- [Spacing](/foundations/spacing/): The space scale
- [Radius and borders](/foundations/radius/): Radius roles and border widths
- [Elevation](/foundations/elevation/): Shadows for floating layers
- [Accessibility](/foundations/accessibility/): Contrast grid of every gated color pair, in every brand and theme
- [All tokens](/tokens/): Every semantic token with its light and dark value, ready to copy
- [Components](/components/): Every component with its guidance
- [Install](/install/): Packages, fonts and the DS Bridge Figma plugin
- [Agents](/agents/): MCP server, bridge CLI and llms.txt for AI agents
- [How review works](/how-review-works/): Risk tiers, review gates, the owner override and the rules the AI agent follows
- [Changelog](/changelog/): Release notes, ready to post
## Studio pages (change: drafts, branches, reviews, versions)
- [Home](/studio/): What waits for you, suggestions and activity
- [Explorations](/studio/explorations/): Every exploration, what waits on you first, from exploring to shipped
- [Library review](/studio/library-review/): What enters the library: every piece an exploration asks for, decided one by one or as a list
- [Being built](/studio/building/): Changes sent from Figma that developers are building, where each one stands, and the next release
- [Components](/studio/components/): Status, tokens, code and Figma links per component
- [Tokens](/studio/tokens/): Edit tokens into a draft, with live preview and guardrails
- [Quality](/studio/quality/): Contrast gate, token validation, docs completeness and adoption, most severe first
- [Settings](/studio/settings/): GitHub connection, the repository this workspace reads, and who approves what
## Rules for agents (from AGENTS.md)
### Always
- Work on a branch named agent/<short-topic>. One concern per PR.
- Use semantic or component tokens for color, elevation and focus: var(--color-action-primary-bg), never a primitive (--color-orange-500) and never a literal.
- Use the space, size, radius, type and motion scales. If a value is missing, stop and ask.
- Accessible by default: native elements or Base UI parts, visible focus, labels wired, color never the only signal.
- Write UI copy in the Bridge voice: sentence case, verbs on buttons, errors that say how to fix. No em dashes.
- Add a changeset. Pick the bump with the rules in .changeset/README.md.
- Fill the PR template, including the risk tier, and say the change is agent-authored.
### Ask before
- Adding, renaming or removing any token (Tier 3, needs design and tech owner approval).
- Adding a new component or a new variant or prop (Tier 2, open as draft PR).
- Changing a public prop name, default or type (breaking change, major).
- Adding a dependency, or upgrading @base-ui/react.
- Touching .stylelintrc.json, .github/, AGENTS.md, CLAUDE.md, docs/GOVERNANCE.md or contrast.checks.json.
### Never
- Never write hex, rgb(), named colors, raw box-shadow, px/rem lengths, ms/s durations, cubic-bezier() or raw z-index values in component CSS.
- Never disable or loosen a lint rule or a contrast pair, add stylelint-disable, or use !important.
- Never delete or skip tests or stories to make a check pass.
- Never edit packages/tokens/dist/: it is generated.
- Never treat a Figma file as the source of truth or copy values from it into code. Figma explores; tokens decide.
- Never merge, push to main, or publish. Humans merge.
## Semantic tokens
Use these in components for color, elevation and focus. Format: CSS variable | light | dark.
### color.bg
Page backgrounds. default for the main canvas, subtle for side panels and zones that sit behind content.
- `var(--color-bg-default)` | palette.neutral.0 (#ffffff) | palette.neutral.950 (#0b1825) | Page canvas. White in bridge, so content sits on paper; ocean keeps a tinted canvas.
- `var(--color-bg-subtle)` | palette.neutral.100 (#edeff2) | palette.neutral.900 (#15202b) | Recessed areas: table headers, code blocks, footers.
- `var(--color-bg-muted)` | palette.neutral.200 (#e3e8ee) | palette.neutral.800 (#323e4c) | Tracks, skeletons, neutral fills.
- `var(--color-bg-zone)` | palette.primary.200 (#cfe8ff) | palette.primary.900 (#00274b) | Large flat color blocks that open a space: the home hero, section page headers, the Studio greeting, onboarding. Never behind dense UI.
### color.surface
Anything that sits on the page: cards, inputs, menus. raised is for surfaces that also carry elevation.
- `var(--color-surface-default)` | palette.neutral.0 (#ffffff) | palette.neutral.900 (#15202b) | Panels, cards, inputs.
- `var(--color-surface-raised)` | palette.neutral.0 (#ffffff) | palette.neutral.800 (#323e4c) | Floating layers: popovers, menus, dialogs, toasts.
- `var(--color-surface-hover)` | palette.neutral.100 (#edeff2) | palette.neutral.800 (#323e4c) | Hover fill for rows, menu items, ghost controls.
- `var(--color-surface-hover-raised)` | palette.neutral.100 (#edeff2) | palette.neutral.700 (#495562) | Hover fill inside a raised surface.
- `var(--color-surface-pressed)` | palette.neutral.200 (#e3e8ee) | palette.neutral.700 (#495562) | Pressed fill.
- `var(--color-surface-selected)` | palette.accent.50 (#f1f8ff) | palette.accent.950 (#001732) | Selected rows and items. Paired with a checked control or a mark, never alone.
- `var(--color-surface-inverse)` | palette.neutral.900 (#15202b) | palette.neutral.100 (#edeff2) | Tooltips and inverse chips.
- `var(--color-surface-strong)` | palette.primary.700 (#0b4a8b) | palette.primary.700 (#0b4a8b) | Deep Harbor in both themes, for a mode the whole page is in (Banner appearance strong). Text on it: text.on-solid. Dark keeps it dark, so it never reads as the Harbor zone or the light primary action of the dark theme.
### color.text
Text and icons. default for body copy, muted for secondary info, disabled only for disabled controls, inverse on dark or brand fills, on-solid on feedback solid fills.
- `var(--color-text-default)` | palette.neutral.900 (#15202b) | palette.neutral.50 (#f5f7f9)
- `var(--color-text-muted)` | palette.neutral.600 (#5b6672) | palette.neutral.400 (#a0aab5) | Secondary text. 4.5:1 on every background.
- `var(--color-text-disabled)` | palette.neutral.400 (#a0aab5) | palette.neutral.600 (#5b6672) | Disabled text. Exempt from contrast minimums; never the only cue.
- `var(--color-text-inverse)` | palette.neutral.0 (#ffffff) | palette.neutral.950 (#0b1825) | Text on surface.inverse.
- `var(--color-text-on-solid)` | palette.neutral.0 (#ffffff) | palette.neutral.0 (#ffffff) | Text and icons on feedback and danger solids.
- `var(--color-text-link)` | palette.link.700 (#0b4a8b) | palette.link.300 (#aad2fe)
- `var(--color-text-link-hover)` | palette.link.800 (#043e70) | palette.link.200 (#cfe8ff)
- `var(--color-text-signal)` | palette.accent.700 (#0b4a8b) | palette.accent.300 (#aad2fe) | Signal-colored text (match highlights, current step).
### color.border
Dividers and outlines. default for quiet separators, strong for control edges that must be found.
- `var(--color-border-default)` | palette.neutral.200 (#e3e8ee) | palette.neutral.800 (#323e4c) | Hairlines: card edges, dividers. Decorative, no contrast minimum.
- `var(--color-border-subtle)` | palette.neutral.100 (#edeff2) | palette.neutral.900 (#15202b) | Row separators inside tables and lists.
- `var(--color-border-strong)` | palette.neutral.500 (#808a96) | palette.neutral.500 (#808a96) | Edges that identify a control (inputs, checkboxes, switches). 3:1 against surfaces (WCAG 1.4.11).
- `var(--color-border-emphasis)` | palette.neutral.300 (#c6cfd9) | palette.neutral.700 (#495562) | A hairline that must read before the default one: hovered cards, group dividers, dashed placeholders. Decorative, no contrast minimum.
- `var(--color-border-hover)` | palette.neutral.600 (#5b6672) | palette.neutral.400 (#a0aab5) | Control edge on hover.
### color.action
Interactive controls only. Never use action colors for decoration, or users will try to click it.
- `var(--color-action-primary-bg)` | palette.primary.700 (#0b4a8b) | palette.primary.300 (#aad2fe) | Primary action fill (steel ink in the bridge brand).
- `var(--color-action-primary-bg-hover)` | palette.primary.800 (#043e70) | palette.primary.200 (#cfe8ff)
- `var(--color-action-primary-bg-active)` | palette.primary.900 (#00274b) | palette.primary.100 (#e4f1fe)
- `var(--color-action-primary-text)` | palette.neutral.0 (#ffffff) | palette.primary.950 (#001732)
- `var(--color-action-secondary-bg)` | palette.neutral.0 (#ffffff) | palette.neutral.900 (#15202b)
- `var(--color-action-secondary-bg-hover)` | palette.neutral.100 (#edeff2) | palette.neutral.800 (#323e4c)
- `var(--color-action-secondary-bg-active)` | palette.neutral.200 (#e3e8ee) | palette.neutral.700 (#495562)
- `var(--color-action-secondary-text)` | palette.neutral.900 (#15202b) | palette.neutral.50 (#f5f7f9)
- `var(--color-action-secondary-border)` | palette.neutral.300 (#c6cfd9) | palette.neutral.600 (#5b6672)
- `var(--color-action-ghost-bg-hover)` | palette.neutral.100 (#edeff2) | palette.neutral.800 (#323e4c)
- `var(--color-action-ghost-bg-active)` | palette.neutral.200 (#e3e8ee) | palette.neutral.700 (#495562)
- `var(--color-action-ghost-text)` | palette.neutral.800 (#323e4c) | palette.neutral.100 (#edeff2)
- `var(--color-action-danger-bg)` | palette.danger.600 (#c3184c) | palette.danger.600 (#c3184c)
- `var(--color-action-danger-bg-hover)` | palette.danger.700 (#9d0639) | palette.danger.700 (#9d0639)
- `var(--color-action-danger-bg-active)` | palette.danger.800 (#760028) | palette.danger.800 (#760028)
- `var(--color-action-danger-text)` | palette.neutral.0 (#ffffff) | palette.neutral.0 (#ffffff)
- `var(--color-action-disabled-bg)` | palette.neutral.100 (#edeff2) | palette.neutral.800 (#323e4c)
- `var(--color-action-disabled-text)` | palette.neutral.400 (#a0aab5) | palette.neutral.600 (#5b6672)
### color.control
- `var(--color-control-bg)` | palette.neutral.0 (#ffffff) | palette.neutral.900 (#15202b) | Unchecked checkbox, radio, input fill.
- `var(--color-control-checked-bg)` | palette.primary.700 (#0b4a8b) | palette.primary.300 (#aad2fe) | Checked checkbox, radio, switch track. Selection is ink.
- `var(--color-control-checked-bg-hover)` | palette.primary.800 (#043e70) | palette.primary.200 (#cfe8ff)
- `var(--color-control-checked-fg)` | palette.neutral.0 (#ffffff) | palette.primary.950 (#001732) | Check mark, radio dot, switch thumb when checked.
- `var(--color-control-track)` | palette.neutral.500 (#808a96) | palette.neutral.500 (#808a96) | Unchecked switch track and slider rail edge. 3:1 against surfaces.
- `var(--color-control-thumb)` | palette.neutral.0 (#ffffff) | palette.neutral.50 (#f5f7f9)
### color.signal
- `var(--color-signal-default)` | palette.accent.600 (#1b6eb9) | palette.accent.400 (#6eaef2) | The signal: active indicators, progress, slider range.
- `var(--color-signal-subtle)` | palette.accent.100 (#e4f1fe) | palette.accent.900 (#00274b) | Soft signal fill: highlighted search matches.
### color.overlay
- `var(--color-overlay-scrim)` | color.alpha.steel-48 (rgb(13 24 37 / 0.48)) | color.alpha.black-64 (rgb(0 0 0 / 0.64)) | Backdrop behind dialogs and drawers.
### color.feedback
Status and system messages. bg + text + border for inline messages, solid for compact labels like badges. Always pair with words.
- `var(--color-feedback-neutral-bg)` | palette.neutral.100 (#edeff2) | palette.neutral.800 (#323e4c) | Fill of a status with no tone (neutral Badge, grey status chips): one step off the page and off cards in both themes. bg.subtle is the card color in dark, so it vanished there.
- `var(--color-feedback-success-bg)` | palette.success.50 (#eefaf6) | palette.success.950 (#001d16)
- `var(--color-feedback-success-border)` | palette.success.200 (#c4ece1) | palette.success.800 (#06483c)
- `var(--color-feedback-success-text)` | palette.success.700 (#036252) | palette.success.300 (#9eddcc)
- `var(--color-feedback-success-icon)` | palette.success.600 (#007e6a) | palette.success.400 (#62bda7)
- `var(--color-feedback-success-solid)` | palette.success.600 (#007e6a) | palette.success.600 (#007e6a)
- `var(--color-feedback-warning-bg)` | palette.warning.50 (#fef6ea) | palette.warning.950 (#221400)
- `var(--color-feedback-warning-border)` | palette.warning.200 (#f8deb6) | palette.warning.800 (#533700)
- `var(--color-feedback-warning-text)` | palette.warning.700 (#6f4c03) | palette.warning.300 (#fad091)
- `var(--color-feedback-warning-icon)` | palette.warning.600 (#8e6201) | palette.warning.400 (#e9b154)
- `var(--color-feedback-warning-solid)` | palette.warning.600 (#8e6201) | palette.warning.600 (#8e6201)
- `var(--color-feedback-danger-bg)` | palette.danger.50 (#fff4f4) | palette.danger.950 (#34000c)
- `var(--color-feedback-danger-border)` | palette.danger.200 (#ffd6da) | palette.danger.800 (#760028)
- `var(--color-feedback-danger-text)` | palette.danger.700 (#9d0639) | palette.danger.300 (#ffb8bf)
- `var(--color-feedback-danger-icon)` | palette.danger.600 (#c3184c) | palette.danger.400 (#fd798e)
- `var(--color-feedback-danger-solid)` | palette.danger.600 (#c3184c) | palette.danger.600 (#c3184c)
- `var(--color-feedback-info-bg)` | palette.info.50 (#f9f5ff) | palette.info.950 (#1f0c30)
- `var(--color-feedback-info-border)` | palette.info.200 (#eadafe) | palette.info.800 (#4b2a6b)
- `var(--color-feedback-info-text)` | palette.info.700 (#653d8c) | palette.info.300 (#dbc0fc)
- `var(--color-feedback-info-icon)` | palette.info.600 (#7f53ac) | palette.info.400 (#bb96e5)
- `var(--color-feedback-info-solid)` | palette.info.600 (#7f53ac) | palette.info.600 (#7f53ac)
### color.chart
- `var(--color-chart-series-1)` | palette.accent.500 (#3f8dda) | palette.accent.400 (#6eaef2) | Series 1, the signal hue: the series a chart is about.
- `var(--color-chart-series-2)` | palette.info.600 (#7f53ac) | palette.info.400 (#bb96e5) | Series 2, the info hue.
- `var(--color-chart-series-3)` | palette.success.600 (#007e6a) | palette.success.400 (#62bda7) | Series 3, the success hue.
- `var(--color-chart-series-4)` | palette.warning.600 (#8e6201) | palette.warning.400 (#e9b154) | Series 4, the warning hue.
- `var(--color-chart-series-5)` | palette.categorical.600 (#bb370b) | palette.categorical.400 (#fb8160) | Series 5, the categorical extra hue.
- `var(--color-chart-series-6)` | palette.neutral.500 (#808a96) | palette.neutral.400 (#a0aab5) | Series 6, neutral: "other" and baselines.
- `var(--color-chart-track)` | palette.neutral.100 (#edeff2) | palette.neutral.800 (#323e4c) | Unfilled part of a bar, gauge or donut.
- `var(--color-chart-grid)` | palette.neutral.200 (#e3e8ee) | palette.neutral.800 (#323e4c) | Grid lines and axis rules.
- `var(--color-chart-axis)` | palette.neutral.600 (#5b6672) | palette.neutral.400 (#a0aab5) | Axis labels and tick text. Same contrast as muted text.
### color.selection
- `var(--color-selection-bg)` | palette.accent.100 (#e4f1fe) | palette.accent.800 (#043e70) | Text selection highlight (::selection).
### color.agent
- `var(--color-agent-surface)` | palette.primary.200 (#cfe8ff) | palette.primary.900 (#00274b) | Tile behind the AI agent mark (Avatar kind agent): the Harbor zone color, with the mark in action.primary.bg (Deep Harbor), the zone pair the build already checks.
### color.illustration
- `var(--color-illustration-surface)` | palette.primary.200 (#cfe8ff) | palette.primary.300 (#aad2fe) | Behind line drawings (the Avatar illustrated faces): the Harbor zone color, kept light in dark so the black lines stay readable.
### focus
Keyboard focus ring. Shared by every focusable component so focus looks the same everywhere.
- `var(--focus-ring-color)` | palette.accent.700 (#0b4a8b) | palette.accent.400 (#6eaef2) | Keyboard focus ring. 3:1 against every surface.
- `var(--focus-ring-width)` | border.width.2 (2px) | border.width.2 (2px)
- `var(--focus-ring-offset)` | border.width.2 (2px) | border.width.2 (2px)
### elevation
Shadows that lift surfaces. raised for cards, overlay for menus and dialogs. Stronger in dark mode so edges stay visible.
- `var(--elevation-raised)` | shadow.1 (0px 1px 2px 0px rgb(13 24 37 / 0.06), 0px 2px 6px -1px rgb(13 24 37 / 0.08)) | shadow.dark-1 (0px 1px 2px 0px rgb(0 0 0 / 0.32), 0px 2px 6px -1px rgb(0 0 0 / 0.24)) | Interactive cards on hover, sticky headers.
- `var(--elevation-overlay)` | shadow.2 (0px 2px 4px -1px rgb(13 24 37 / 0.08), 0px 12px 24px -6px rgb(13 24 37 / 0.14)) | shadow.dark-2 (0px 2px 4px -1px rgb(0 0 0 / 0.4), 0px 12px 24px -6px rgb(0 0 0 / 0.44)) | Popovers, menus, listboxes, tooltips.
- `var(--elevation-modal)` | shadow.3 (0px 4px 8px -2px rgb(13 24 37 / 0.1), 0px 24px 48px -12px rgb(13 24 37 / 0.22)) | shadow.dark-3 (0px 4px 8px -2px rgb(0 0 0 / 0.48), 0px 24px 48px -12px rgb(0 0 0 / 0.56)) | Dialogs, drawers, toasts.
### motion
- `var(--motion-duration-instant)` | duration.75 (75ms) | duration.75 (75ms) | Hover and press color changes.
- `var(--motion-duration-fast)` | duration.150 (150ms) | duration.150 (150ms) | Tooltips, menus, popovers, checkbox ticks.
- `var(--motion-duration-moderate)` | duration.200 (200ms) | duration.200 (200ms) | Dialogs, accordions, tab indicator.
- `var(--motion-duration-slow)` | duration.300 (300ms) | duration.300 (300ms) | Drawers, toasts entering.
- `var(--motion-duration-spin)` | duration.700 (700ms) | duration.700 (700ms) | One spinner rotation.
- `var(--motion-duration-shimmer)` | duration.1400 (1400ms) | duration.1400 (1400ms) | One skeleton shimmer pass.
- `var(--motion-easing-standard)` | easing.standard (cubic-bezier(0.2, 0, 0, 1)) | easing.standard (cubic-bezier(0.2, 0, 0, 1))
- `var(--motion-easing-enter)` | easing.expo-out (cubic-bezier(0.16, 1, 0.3, 1)) | easing.expo-out (cubic-bezier(0.16, 1, 0.3, 1))
- `var(--motion-easing-exit)` | easing.accel (cubic-bezier(0.4, 0, 1, 1)) | easing.accel (cubic-bezier(0.4, 0, 1, 1))
- `var(--motion-easing-linear)` | easing.linear (cubic-bezier(0, 0, 1, 1)) | easing.linear (cubic-bezier(0, 0, 1, 1))
### layer
- `var(--layer-sticky)` | z.sticky (100) | z.sticky (100)
- `var(--layer-dropdown)` | z.dropdown (1000) | z.dropdown (1000)
- `var(--layer-overlay)` | z.overlay (1100) | z.overlay (1100)
- `var(--layer-modal)` | z.modal (1200) | z.modal (1200)
- `var(--layer-popover)` | z.popover (1300) | z.popover (1300)
- `var(--layer-toast)` | z.toast (1400) | z.toast (1400)
- `var(--layer-tooltip)` | z.tooltip (1500) | z.tooltip (1500)
### icon
- `var(--icon-size-xs)` | size.3 (12px) | size.3 (12px)
- `var(--icon-size-sm)` | size.4 (16px) | size.4 (16px)
- `var(--icon-size-md)` | size.5 (20px) | size.5 (20px)
- `var(--icon-size-lg)` | size.6 (24px) | size.6 (24px)
- `var(--icon-stroke)` | 1.75 | 1.75 | Lucide stroke width at every size, matched to Plex text weight.
### border-width
- `var(--border-width-default)` | border.width.1 (1px) | border.width.1 (1px)
- `var(--border-width-strong)` | border.width.2 (2px) | border.width.2 (2px)
- `var(--border-width-indicator)` | border.width.2 (2px) | border.width.2 (2px)
### layout
- `var(--layout-sidebar-width)` | size.64 (256px) | size.64 (256px) | App shell side navigation width.
- `var(--layout-header-height)` | size.14 (56px) | size.14 (56px) | App shell top bar height. Sticky offsets add it.
- `var(--layout-content-max-width)` | size.300 (1200px) | size.300 (1200px) | Widest a page content column grows before it stays left aligned.
## Scales (primitives, same in both themes)
- Spacing: `--space-0` 0px, `--space-1` 4px, `--space-2` 8px, `--space-3` 12px, `--space-4` 16px, `--space-5` 20px, `--space-6` 24px, `--space-7` 28px, `--space-8` 32px, `--space-10` 40px, `--space-12` 48px, `--space-16` 64px, `--space-20` 80px, `--space-24` 96px, `--space-0-5` 2px, `--space-1-5` 6px
- Radius: `--radius-none` 0px, `--radius-xs` 2px, `--radius-sm` 4px, `--radius-md` 6px, `--radius-lg` 10px, `--radius-xl` 12px, `--radius-2xl` 16px, `--radius-3xl` 20px, `--radius-full` 9999px
- Border width: `--border-width-1` 1px, `--border-width-2` 2px, `--border-width-3` 3px
- Font size: `--font-size-100` 12px, `--font-size-200` 14px, `--font-size-300` 16px, `--font-size-400` 21px, `--font-size-500` 24px, `--font-size-600` 30px, `--font-size-700` 36px, `--font-size-800` 44px, `--font-size-900` 50px, `--font-size-1000` 64px
- Font weight: `--font-weight-regular` 400, `--font-weight-medium` 500, `--font-weight-semibold` 600, `--font-weight-bold` 700
- Line height: `--font-line-height-none` 1, `--font-line-height-tight` 1.2, `--font-line-height-snug` 1.35, `--font-line-height-normal` 1.5, `--font-line-height-relaxed` 1.65
If a value is not in a scale, do not invent one: propose a token in the PR.
## Contrast
Pairs are listed in packages/tokens/src/contrast.checks.json and checked against WCAG AA (4.5:1 text, 3:1 UI).
All listed pairs pass AA in both themes.
## Components
### Icon
Draws a Lucide icon at a token size with the system stroke width, in the color of the text around it. Decorative by default: pass a label only when the icon carries meaning on its own.
Import: `import { Icon } from '@bridge-ds/react'`
Props:
- icon: LucideIcon. A Lucide icon component, for example `import { GitBranch } from 'lucide-react'`.
- size?: 'xs' | 'sm' | 'md' | 'lg' = 'sm'. Token size: xs 12, sm 16, md 20, lg 24. Default `sm`, the size used inside controls.
- label?: string. Accessible name. Omit for decorative icons next to text (they are hidden from assistive tech).
When to use:
- Next to a label, to help people scan: a menu item, a tab, a status line, a table cell.
- Inside your own controls, at size sm (16), the size the system uses in controls.
- In navigation at size md (20), so the side nav reads at a glance.
- Any Lucide glyph: pass the component itself, for example icon={GitBranch} imported from lucide-react.
When not to use:
- As the only content of a button: use IconButton, which requires a label and sizes the hit area.
- Inside Button or Badge when they place the icon for you: Button takes iconStart and iconEnd, Badge takes icon.
- Emoji or Unicode glyphs standing in for icons: the system uses one family, Lucide.
- Illustrations or logos: Icon draws 24px grid line icons only.
Do:
- Pair the icon with text. The word carries the meaning and the icon helps people find it. The icon stays hidden from screen readers, so the label is read once.
- Pick the size from the token scale. xs 12, sm 16, md 20, lg 24. The stroke stays at icon.stroke on every size, so icons keep the weight of the text.
- Give a label when the icon stands alone and means something. With label, Icon becomes role="img" with that accessible name. Say what it means ("Merged"), not what it shows ("Purple circle").
Don't:
- Set a color or a size by hand. Icon inherits currentColor and takes its size from the icon size tokens. A hard-coded width or color drifts from the theme and the density.
- Rely on the icon alone to say a status. Color and shape are not enough for everyone. Put the status in words next to it, or use Badge.
Accessibility:
- Without label, the icon gets aria-hidden="true": screen readers skip it and read the text next to it.
- With label, it gets role="img" and aria-label, so it is announced as an image with that name.
- focusable="false" keeps the SVG out of the tab order. Make the parent control focusable, never the icon.
- The icon inherits the text color, so it gets the same contrast as the label it sits next to.
Tokens used: --icon-size-lg, --icon-size-md, --icon-size-sm, --icon-size-xs, --icon-stroke
Docs facts: [/docs-ai/icon.json](/docs-ai/icon.json)
### VisuallyHidden
Text for assistive technology only: screen readers read it, the screen does not show it. Use it to give a control or a symbol the words sighted users get from the layout.
Import: `import { VisuallyHidden } from '@bridge-ds/react'`
Accepts all native <span> attributes.
When to use:
- A control whose visible content is not enough on its own, like a checkbox in a table row: "Select Button".
- Extra context a sighted user gets from the layout, like " (opens in a new tab)" after an external link.
- A label for a loading region while Skeleton placeholders are shown, like "Loading tokens".
When not to use:
- Hiding content from everyone: use the hidden attribute or do not render it.
- Naming an icon-only button: IconButton takes a label prop for that.
- Long text or anything interactive: it is a span clipped to one pixel, so a link or button inside it would take focus while staying invisible.
Do:
- Add the words the layout implies. Link with external does it for you: it adds a VisuallyHidden " (opens in a new tab)" after the label.
- Keep it short and specific. Name the thing the control acts on: "Select Button" in a row, "Select all tokens" in the header.
Don't:
- Repeat what is already visible. If the visible label says it, screen readers read it twice.
- Use it to hide content you want to remove. Screen reader users still hear it. Remove the content, or use the hidden attribute.
Accessibility:
- Renders a span that stays in the accessibility tree: screen readers read its text in place.
- Clipped to one pixel with clip-path and overflow hidden, positioned absolutely, so it takes no room in the layout.
- white-space: nowrap keeps the words from being read one per line by some screen readers.
- Accepts every native span attribute and spreads them, so an id for aria-describedby works.
Tokens used: --border-width-1
Docs facts: [/docs-ai/visually-hidden.json](/docs-ai/visually-hidden.json)
### Button
Triggers an action on the current screen: save, publish, approve, open a dialog. If it takes the user somewhere else, it is a Link, not a Button.
Import: `import { Button } from '@bridge-ds/react'`
Accepts all native <button> attributes.
Props:
- variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'link' | 'inverse' = 'primary'. Visual emphasis. Use one primary action per view; danger only for destructive actions.
- size?: 'sm' | 'md' | 'lg' = 'md'. Height follows the density tokens: sm 32/28, md 40/32, lg 48/40 (comfortable/compact).
- loading?: boolean = false. Shows a spinner, keeps the button focusable, ignores clicks and sets aria-busy.
- iconStart?: LucideIcon. Lucide icon before the label. Replaced by the spinner while loading.
- iconEnd?: LucideIcon. Lucide icon after the label, for example a chevron on a menu trigger.
- fullWidth?: boolean = false. Stretch to the width of the container.
- pressed?: boolean. Makes the button a toggle: sets `aria-pressed` and shows the selected look when true. Leave unset for a plain button. A script can flip `aria-pressed` on a statically rendered toggle: the look follows the attribute.
- render?: ReactElement. Render another element with button styling, for example a router link: `render={<a href="/tokens" />}`. The element keeps its own role (a link stays a link), so only use it for navigation.
When to use:
- The user commits an action on this screen: Publish, Approve, Create token, Open pull request.
- Primary: the one action you most want the user to take in this view. One per form, dialog or page section.
- Secondary: the alternatives next to the primary, like Save as draft or Request changes.
- Ghost: low-emphasis actions repeated in lists, tables or toolbars (Edit, View diff), where filled buttons would fight for attention.
- Danger: destructive actions (Delete, Revoke access). Usually inside a confirmation dialog.
- Pressed: a toggle that stays on, like Compare or Show what changed in a toolbar. Set pressed to true or false; the selected look and aria-pressed follow it.
- Link: an action on this screen inside a sentence, like "Show only these" after a count. It looks like a link and stays a button.
- Inverse: the action on a strong surface (Deep Harbor, the inverse terminal bars), where secondary would disappear.
- Size sm in dense rows and toolbars, md everywhere else, lg only for a single call to action on an empty or onboarding screen. Heights follow the density setting.
When not to use:
- Going to another page or URL: use Link. Links and buttons are announced differently and users expect Cmd+click to open links in a new tab.
- Navigation with variant link: a link-looking button still does not go anywhere. Use Link to go to another page.
- Turning a setting on or off in a form: use Switch or Checkbox. A pressed Button is for toolbar toggles that change the view.
- Picking one of several views: use Tabs or an attached ButtonGroup.
- More than three actions in one row: keep the main ones and move the rest to a Menu behind an IconButton.
- To show a status that cannot be clicked: use a Badge.
Do:
- Start the label with a verb that names the outcome. "Publish tokens" and "Approve change" tell the user what happens before they click.
- One primary per view, the others step down. Hierarchy tells users where to look first. Secondary and ghost carry the alternatives.
- Show progress with loading, keep the label. The button stays in place and focusable, so keyboard users do not lose their position. Change the label to the present participle.
- A toggle says its state with pressed. Pass pressed true or false, never swap variants to show the state. The look, aria-pressed and a script driving a static page stay in step.
Don't:
- Two primary buttons side by side. When everything is primary, nothing is. Users hesitate and pick at random.
- Vague labels. "OK" and "Click here" say nothing out of context, and screen reader users often list buttons out of context.
- Danger for actions that are not destructive. Red trains people to ignore red. Keep danger for actions that remove or revoke something.
Accessibility:
- Native button through Base UI: Enter and Space activate it, it is in the tab order, no extra ARIA needed.
- The focus ring (focus.ring.color, 2px, offset 2px) shows on keyboard focus only, never on mouse click.
- Loading sets aria-busy and aria-disabled, ignores clicks, and keeps the button focusable so keyboard users keep their place.
- Disabled removes the button from the tab order and explains nothing. Prefer an enabled button plus a message saying what is missing.
- pressed sets aria-pressed, so screen readers announce a toggle button and its state. Leave it unset on plain buttons: aria-pressed false still announces a toggle.
- Variant link keeps the native button: Enter and Space work, it is announced as a button, and the focus ring is the DS ring.
- With render={<a href />} the element stays a link (role link), which is right for navigation styled as a button.
- Heights are 32/40/48px comfortable and 28/32/40px compact: all above the WCAG 2.2 minimum target of 24px.
Tokens used: --border-width-default, --border-width-strong, --button-danger-bg, --button-danger-bg-active, --button-danger-bg-hover, --button-danger-text, --button-font-weight, --button-ghost-bg-active, --button-ghost-bg-hover, --button-ghost-text, --button-primary-bg, --button-primary-bg-active, --button-primary-bg-hover, --button-primary-text, --button-radius, --button-secondary-bg, --button-secondary-bg-active, --button-secondary-bg-hover, --button-secondary-border, --button-secondary-text, --color-action-disabled-bg, --color-action-disabled-text, --color-control-checked-bg, --color-control-checked-bg-hover, --color-surface-default, --color-surface-hover, --color-surface-pressed, --color-surface-selected, --color-text-default, --color-text-link, --color-text-link-hover, --control-gap, --control-height-lg, --control-height-md, --control-height-sm, --control-padding-x-lg, --control-padding-x-md, --control-padding-x-sm, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-sans, --font-line-height-tight, --font-size-200, --font-size-300, --font-weight-semibold, --motion-duration-instant, --motion-easing-standard, --radius-inset, --space-0-5, --space-1, --space-1-5
Docs facts: [/docs-ai/button.json](/docs-ai/button.json)
### ButtonGroup
Lays out related buttons as one unit, either spaced (an action bar or dialog footer) or attached (a segmented control), and gives the group one accessible name.
Import: `import { ButtonGroup } from '@bridge-ds/react'`
Accepts all native <div> attributes.
Props:
- appearance?: 'spaced' | 'attached' = 'spaced'. Accessible name for the group, for example "Text alignment". Required when the buttons are related choices. */ 'aria-label'?: string; /** `attached` joins the buttons into one segmented control; `spaced` keeps a gap (action bars). Default `spaced`.
- orientation?: 'horizontal' | 'vertical' = 'horizontal'
- align?: 'start' | 'end' | 'between' = 'start'. Align the group's buttons. Default `start`. Use `end` for dialog and form footers.
When to use:
- Footers of dialogs, forms and cards: primary at the end, alternatives before it.
- Attached: two to five closely related choices of equal weight, like Unified or Split diff.
When not to use:
- Switching between views of content: use Tabs, which manage the panel and arrow-key navigation.
- A single choice that persists as a setting: use RadioGroup.
Do:
- Put the primary action last. Users read the alternatives on the way to the main action, and the primary lands where the eye ends.
Don't:
- Mix variants in an attached group. An attached group reads as one control. Mixed fills break it into unrelated pieces.
Accessibility:
- Renders role="group". Pass aria-label when the buttons are related choices so the group is announced.
- Each button stays in the tab order; the focused button lifts above its neighbors so the ring is never clipped.
- Vertical groups stack the same buttons; the tab order follows the visual order.
Tokens used: --border-width-default, --space-2
Docs facts: [/docs-ai/button-group.json](/docs-ai/button-group.json)
### IconButton
A square button with only an icon, for frequent, recognizable actions in toolbars, table rows and headers. Its label is still required: it becomes the accessible name.
Import: `import { IconButton } from '@bridge-ds/react'`
Accepts all native <button> attributes except children, aria-label.
Props:
- icon: LucideIcon. Lucide icon component.
- label: string. Accessible name, required: there is no visible text. Say the action, not the icon ("Delete token", not "Trash").
- variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | 'inverse' = 'ghost'. Default `ghost`: icon buttons usually live in toolbars and rows. `inverse` on strong surfaces.
- size?: ButtonSize = 'md'
- loading?: boolean = false
- pressed?: boolean. Makes the button a toggle: sets `aria-pressed` and shows the selected look when true. Leave unset otherwise.
When to use:
- Repeated row actions in tables and lists: edit, copy, delete, more.
- Toolbars where space is tight and the icons are widely understood (close, copy, search, settings).
- Opening a Menu of secondary actions (the more icon).
When not to use:
- The primary action of a view: give it a text Button.
- Any icon whose meaning is not obvious without its label: use a Button with iconStart instead.
- Navigation to another page: use a Link.
Do:
- Name the action, not the picture. label="Delete token" is what a screen reader says. "Trash" describes the drawing, not what happens.
- Pair with a Tooltip showing the same label. Sighted mouse users get the name on hover; keyboard users get it on focus.
Don't:
- A row of filled icon buttons. Primary icon buttons in every row shout. Keep row actions ghost.
Accessibility:
- label is required and becomes aria-label. The icon itself is aria-hidden.
- Same keyboard behavior and focus ring as Button. Loading keeps focus and sets aria-busy.
- The square target matches the control height (32/40/48 comfortable), never smaller than 24px.
Tokens used: --font-line-height-none
Docs facts: [/docs-ai/icon-button.json](/docs-ai/icon-button.json)
### Link
Takes the user to another page, section or site. Underlined in running text so it is never identified by color alone.
Import: `import { Link } from '@bridge-ds/react'`
Accepts all native <a> attributes.
Props:
- variant?: 'inline' | 'standalone' | 'subtle' = 'inline'. `inline` sits in running text (always underlined); `standalone` stands alone (underline on hover); `subtle` inherits the text color. Default `inline`.
- external?: boolean = false. Opens in a new tab: adds target, rel, an icon and "(opens in a new tab)" for screen readers.
- render?: ReactElement. Render a router link instead of `<a>`: `render={<RouterLink to="/tokens" />}`.
When to use:
- Navigation: another page, a section of this page, an external resource.
- Inline in sentences (inline variant), or on its own under a card or a table (standalone).
- external for other sites: it opens a new tab and says so.
When not to use:
- Actions on this screen (save, delete, open a dialog): use Button.
- Navigation that should look like a button (a single call to action): use Button with render={<a href />}.
Do:
- Link text says where it goes. Screen reader users jump between links out of context: "Read the token guidelines" works alone, "here" does not.
Don't:
- "Click here" or a bare URL. Both say nothing about the destination.
Accessibility:
- Native <a href>: Enter follows it, it is announced as a link, Cmd or Ctrl+click opens a new tab.
- Inline links are underlined (WCAG 1.4.1): color is never the only cue inside text.
- external adds target=_blank, rel=noopener noreferrer, an icon and "(opens in a new tab)" for screen readers.
- Focus ring hugs the text with a 1px offset so it does not overlap neighbouring lines.
Tokens used: --border-width-1, --border-width-2, --color-border-strong, --color-text-link, --color-text-link-hover, --focus-ring-color, --focus-ring-width, --font-weight-medium, --motion-duration-instant, --motion-easing-standard, --radius-inset, --size-6, --space-0-5
Docs facts: [/docs-ai/link.json](/docs-ai/link.json)
### Checkbox
A single on or off choice with its label next to the box, submitted with a form. Supports a mixed state for a parent that controls a partly checked group.
Import: `import { Checkbox } from '@bridge-ds/react'`
Props:
- label: ReactNode. Visible label next to the box. Clicking it toggles the checkbox.
- description?: ReactNode. Secondary text under the label, linked with aria-describedby.
- checked?: boolean
- defaultChecked?: boolean
- onCheckedChange?: (checked: boolean) => void
- indeterminate?: boolean. Mixed state, for a parent that controls a partly checked group. Announced as "mixed".
- parent?: boolean. Inside a CheckboxGroup with allValues: this checkbox selects or clears the whole group.
- value?: string. Value submitted with the form, and the item id inside a CheckboxGroup.
- name?: string
- disabled?: boolean
- readOnly?: boolean
- required?: boolean
- invalid?: boolean = false. Marks the checkbox invalid (red edge). Pair it with an error message on the group or form.
- id?: string
- className?: string
When to use:
- An independent yes or no that takes effect when the form is saved: agree, include, require.
- Several independent options under one question: put them in a CheckboxGroup.
- Selecting rows in a Table (with a parent checkbox in the header for select all).
When not to use:
- A setting that applies immediately: use Switch.
- One choice among exclusive options: use RadioGroup.
- A command (Delete, Publish): use Button.
Do:
- Phrase the label as the positive case. Checked means the label is true. "Notify reviewers" reads cleanly; a negative label makes users double-check.
- Use the description for consequences. Keep the label short and explain the effect underneath.
Don't:
- Negative labels. "Do not send notifications" checked means off. Users have to think twice.
- A checkbox that acts immediately. If ticking it changes something right away, it is a Switch.
Accessibility:
- Base UI Checkbox: role checkbox, Space toggles, aria-checked is true, false or mixed (indeterminate).
- The whole label row is the click target; the label text is the accessible name (aria-labelledby) and the description is linked with aria-describedby.
- The box edge uses color.border.strong (3:1 against surfaces); checked uses the ink fill with a white check (4.5:1 or more).
- Indeterminate shows a dash, so mixed is not conveyed by color.
- invalid adds a danger edge; always pair it with an error message in the group or form.
Tokens used: --border-width-default, --checkbox-bg, --checkbox-bg-checked, --checkbox-border, --checkbox-fg-checked, --checkbox-radius, --color-action-disabled-bg, --color-action-disabled-text, --color-border-default, --color-border-hover, --color-control-checked-bg-hover, --color-text-default, --color-text-disabled, --color-text-muted, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-size-100, --font-size-200, --font-tracking-wide, --font-weight-regular, --input-border-invalid, --motion-duration-fast, --motion-duration-instant, --motion-easing-enter, --motion-easing-standard, --size-4, --size-6, --space-0-5, --space-2
Docs facts: [/docs-ai/checkbox.json](/docs-ai/checkbox.json)
### CheckboxGroup
Several independent choices under one question, in a fieldset with a legend. An optional parent checkbox selects or clears the whole group and shows a mixed state.
Import: `import { CheckboxGroup } from '@bridge-ds/react'`
Props:
- label: ReactNode. Group label, rendered as the fieldset legend.
- children: ReactNode. Checkbox items, each with a `value`.
- description?: ReactNode. Help under the legend.
- error?: ReactNode. Error for the group, for example "Pick at least one reviewer." Linked with aria-describedby.
- value?: string[]. Values of the checked items (controlled).
- defaultValue?: string[]
- onValueChange?: (value: string[]) => void
- allValues?: string[]. Every item value. Required for a parent "Select all" checkbox: add `<Checkbox parent label="All" />` and the group drives its checked and mixed state.
- orientation?: 'vertical' | 'horizontal' = 'vertical'. Default `vertical`.
- disabled?: boolean
- required?: boolean = false
- hideLabel?: boolean = false
- className?: string
When to use:
- Users may pick zero, one or several options from a short visible list (up to about seven).
- Use allValues plus <Checkbox parent /> for a Select all at the top of a long list.
When not to use:
- Only one option can be picked: use RadioGroup.
- More than about seven options or a searchable list: use Combobox with multiple selection.
Do:
- Ask the question in the legend. The legend is read before each option, so it gives every label its context.
Don't:
- A group without a legend. Screen reader users hear "Light, checkbox" with no idea what it is for.
Accessibility:
- Native fieldset and legend: the legend names the group; disabled on the fieldset disables every item.
- description and error are linked to the fieldset with aria-describedby; the error carries an icon and text.
- The parent checkbox is checked, unchecked or mixed (aria-checked) based on the children, driven by Base UI.
- Each item keeps its own label and description, so items can be reached and understood one by one.
Tokens used: --border-width-1, --color-feedback-danger-icon, --color-feedback-danger-text, --color-text-default, --color-text-disabled, --color-text-muted, --font-family-sans, --font-line-height-normal, --font-line-height-snug, --font-size-100, --font-size-200, --font-tracking-wide, --font-weight-medium, --input-border-invalid, --space-0-5, --space-1, --space-1-5, --space-2, --space-6
Docs facts: [/docs-ai/checkbox-group.json](/docs-ai/checkbox-group.json)
### ChoiceCard
One choice among two to four options that each need a title and a short description, shown as cards: Keep mine or Use the library's, a starting brand, a corner style. The whole card is the click target.
Import: `import { ChoiceCard } from '@bridge-ds/react'`
Props:
- value: string. Value selected when this card is chosen.
- title: ReactNode. The option, as a verb or a short name: "Keep mine", "Use the library's".
- description?: ReactNode. What choosing it means, or the value it keeps (a TokenValue). One or two lines.
- disabled?: boolean
- className?: string
When to use:
- A decision where each option needs a line of explanation or a value to compare: resolving a conflict, picking a starting brand.
- Two to four options. Side by side (horizontal, the default) when the reader compares them; they stack on their own when they no longer fit.
- vertical when the descriptions are long or the options are a ranked list.
- Put a TokenValue in description when the choice is between token values, so the reader sees what each card keeps.
- hideLabel when the row around the group already asks the question; the legend stays the accessible name.
When not to use:
- Options a short label explains on its own: use RadioGroup.
- Switching a view or a mode that applies at once: use SegmentedControl.
- Several options the reader can pick together: use Checkbox.
- More than four options: use RadioGroup or Select.
Do:
- Titles are the choice, descriptions are the consequence. The title says what the reader picks in a few words, the description says what happens or what it keeps.
- Ask the question in the legend. The legend names the decision, as a question or a noun, so the cards read as answers.
Don't:
- Cards for one-word options. When the titles say it all and nothing needs comparing, cards take room for nothing. Use RadioGroup.
- Descriptions that repeat the title. "Keep mine: keeps your value" says it twice. Show the value instead, or what changes.
Accessibility:
- A fieldset with a legend around a Base UI RadioGroup: the legend names the group, each card is a radio named by its title.
- Each card's description is linked to its radio with aria-describedby, so screen readers read it after the title. The group description and error are linked to the group.
- Tab enters the group on the selected card and leaves it; arrow keys move the selection.
- The selected card shows the checked radio, a Deep Harbor edge and the selected surface: never color alone.
- A disabled card is skipped by the arrow keys and cannot be picked; say why in its description.
- The focus ring (focus.ring.color) shows on the radio on keyboard focus only.
Tokens used: --border-width-default, --color-bg-subtle, --color-border-default, --color-border-hover, --color-border-strong, --color-control-checked-bg, --color-control-checked-bg-hover, --color-surface-default, --color-surface-hover, --color-surface-selected, --color-text-default, --color-text-disabled, --color-text-muted, --control-height-lg, --font-family-sans, --font-weight-semibold, --input-border-invalid, --motion-duration-instant, --motion-easing-standard, --radius-control, --size-60, --space-1, --space-3, --space-4, --typography-ui-line-height, --typography-ui-size
Docs facts: [/docs-ai/choice-card.json](/docs-ai/choice-card.json)
### Combobox
A text input that filters a list of options as the user types, then selects one or several of them. Built for long lists and async search (people, repositories, tokens).
Import: `import { Combobox } from '@bridge-ds/react'`
Accepts all native <input> attributes except value, defaultValue, onChange, size, prefix, children.
Props:
- options: ComboboxOption[]. The options to filter. For async search, pass the latest results and set `filter={null}`.
- multiple?: boolean = false. Pick several values, shown as removable chips.
- value?: ComboboxValue. Selected value (controlled): a string, an array with `multiple`, or null.
- defaultValue?: ComboboxValue. Initially selected value (uncontrolled).
- onValueChange?: (value: ComboboxValue) => void. Called with the new value (string, string[] with `multiple`, or null).
- inputValue?: string. Text in the input (controlled). Use it to drive an async search.
- defaultInputValue?: string
- onInputValueChange?: (inputValue: string) => void. Called on every keystroke with the query.
- filter?: ((option: ComboboxOption, query: string) => boolean) | null. Custom matcher, or `null` to show `options` as given (already filtered by a server). Default: case and accent insensitive "contains".
- loading?: boolean = false. Results are loading: shows a spinner row, sets aria-busy and announces `loadingMessage`.
- loadingMessage?: string = 'Loading results'. Announced and shown while loading. Default "Loading results".
- emptyMessage?: ReactNode = 'No matches. Try another name.'. Shown when no option matches. Say what to try next.
- label?: string. Visible label. When set, the Combobox wraps itself in a Field. Leave unset inside a Field.
- hint?: ReactNode. Help under the label (Field description). Only with `label`.
- error?: ReactNode. Error message. Only with `label`.
- hideLabel?: boolean
- size?: 'sm' | 'md' | 'lg' = 'md'. Height follows the density tokens. Default `md`.
- prefix?: ReactNode. Content before the query, usually a search Icon.
- open?: boolean. Popup open state (controlled).
- defaultOpen?: boolean
- onOpenChange?: (open: boolean) => void
- portalContainer?: HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null. Where the popup is rendered. Default: the end of the body, above every layer including a Dialog or Drawer, so the list is never covered. Pass an element (or a ref) to keep it inside a container, for example a shadow root.
When to use:
- Choosing from a long or growing list where typing is faster than scrolling: reviewers, repositories, components, tokens.
- Results come from a server: control inputValue, fetch, pass the results as options with filter={null} and loading while waiting.
- Several values from a long list (multiple): selected values become removable chips inside the field.
- Inside a Dialog or Drawer: the list opens above the dialog and focus stays in it. Use description for a short second line under each label (12px, muted), like a component's family or a person's role.
When not to use:
- Short known lists (under about ten): use Select, which needs no typing.
- Free text with suggestions (a search box): the value must be one of the options here, so use Input or an autocomplete pattern.
- Two or three options: use RadioGroup.
Do:
- Add a second line when names are ambiguous. A role or an email under the name helps pick the right Ada, and is matched by screen readers too.
- Write an empty message that says what to try. "No one matches. Check the spelling or invite them." turns a dead end into a next step.
Don't:
- Spinner with no words. While loading, say what is happening ("Searching people"). The message is also announced to screen readers.
- Combobox for a handful of fixed options. Typing to find one of three themes is slower than seeing them. Use Select or RadioGroup.
Accessibility:
- Base UI Combobox: the input has role combobox with aria-expanded, aria-controls and aria-activedescendant; the popup is a listbox.
- Keyboard: typing filters; Down Arrow opens and moves; Enter selects; Escape closes, then clears; with multiple, Left Arrow from the start of the input moves through the chips and Backspace removes one.
- loading renders the message in a live status region and sets aria-busy on the popup; the empty message is announced the same way.
- The clear and toggle buttons inside the field have accessible names ("Clear selection", "Show options"); each chip remove button says "Remove <name>".
- The Field label names the input; hint and error are linked through aria-describedby.
- The popup is portaled and re-applies the theme, brand and density of its field. It sits on the popover layer, above a Dialog, and the dialog keeps focus management: picking returns focus to the input inside the dialog.
Tokens used: --border-width-default, --color-bg-subtle, --color-border-default, --color-surface-hover, --color-surface-hover-raised, --color-surface-pressed, --color-text-default, --color-text-disabled, --color-text-muted, --control-height-lg, --control-height-md, --control-height-sm, --control-padding-x-md, --control-padding-x-sm, --focus-ring-color, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-line-height-snug, --font-size-100, --font-size-200, --font-size-300, --font-tracking-wide, --font-weight-medium, --input-bg, --input-bg-disabled, --input-border, --input-border-hover, --input-border-invalid, --input-placeholder, --input-radius, --input-text, --layer-popover, --menu-item-height, --motion-duration-fast, --motion-duration-instant, --motion-easing-enter, --motion-easing-exit, --motion-easing-standard, --popup-bg, --popup-border, --popup-radius, --popup-shadow, --radius-inset, --size-20, --size-5, --size-96, --space-0-5, --space-1, --space-2, --space-3
Docs facts: [/docs-ai/combobox.json](/docs-ai/combobox.json)
### Field
Wires a visible label, an optional description and an error message to one form control. Ids, aria-describedby, aria-invalid and validation timing are handled by Base UI Field.
Import: `import { Field, useFieldContext } from '@bridge-ds/react'`
Accepts all native <div> attributes except onChange.
Props:
- label: ReactNode. Visible label. Required: placeholders are not labels.
- children: ReactNode. The control: Input, Textarea, Select, Combobox, NumberField, Slider.
- description?: ReactNode. Help shown under the label, before the control: format, constraints, privacy notes.
- error?: ReactNode. Error message. When set, the field is invalid and the message is announced through aria-describedby. Leave it unset to show the browser's validation message after a failed submit.
- required?: boolean = false. Marks the control as required (adds `required` to our controls and a visual marker to the label).
- disabled?: boolean
- name?: string. Name used by Form for submit values and server errors.
- validate?: (value: unknown) => string | string[] | null | undefined | Promise<string | string[] | null | undefined>. Custom validation. Return an error string (or strings) to mark the field invalid.
- validationMode?: 'onSubmit' | 'onBlur' | 'onChange'. When validation runs. Default `onSubmit` (inherits Form).
- hideLabel?: boolean = false. Visually hide the label (it stays the accessible name). Use only when the context labels the field, like a search bar.
When to use:
- Around every Input, Textarea, Select, Combobox, NumberField or Slider that does not take a label prop itself.
- To show a server-side error next to the right control (error prop, or Form errors by name).
- description for what the user needs before typing: format, limits, why you ask.
When not to use:
- Checkbox and Switch: they carry their own label next to the control.
- A group of checkboxes or radios: use CheckboxGroup or RadioGroup, which render a fieldset and legend.
Do:
- Description before, error after. The description answers questions before typing; the error appears under the control, where the eye already is.
Don't:
- Placeholder as the label. It disappears on the first keystroke and has low contrast. Keep a visible label.
Accessibility:
- The label is a native <label> bound to the control: clicking it focuses the control.
- description and error are linked through aria-describedby; an error also sets aria-invalid on the control.
- Errors carry an icon and text, never color alone (WCAG 1.4.1).
- required shows an asterisk (hidden from screen readers) and sets the native required attribute, which is announced.
- hideLabel keeps the label for assistive tech; use it only when the context labels the field visually (a search bar with an icon).
Tokens used: --border-width-1, --color-feedback-danger-icon, --color-feedback-danger-text, --color-text-default, --color-text-disabled, --color-text-muted, --font-family-sans, --font-line-height-normal, --font-line-height-snug, --font-size-100, --font-size-200, --font-tracking-wide, --font-weight-medium, --space-0-5, --space-1, --space-1-5
Docs facts: [/docs-ai/field.json](/docs-ai/field.json)
### Form
A form with one validation pattern for the whole system: client checks on submit, server errors passed by field name, each shown under its Field and listed in a summary that takes focus.
Import: `import { Form, FormActions } from '@bridge-ds/react'`
Accepts all native <HTMLFormElement> attributes except onSubmit.
Props:
- errors?: Record<string, string | string[]>. Errors returned by the server after submit, keyed by field name. Each one shows under its Field and, with `errorSummary`, in a list at the top. A field's error clears when its value changes.
- onFormSubmit?: (values: Record<string, unknown>) => void. Called with the form values (keyed by name) when the form is valid and submitted.
- validationMode?: 'onSubmit' | 'onBlur' | 'onChange' = 'onSubmit'. When fields validate. Default `onSubmit`, then live once a field has been shown as invalid.
- errorSummary?: boolean = true. Show a summary of `errors` above the fields, with links that move focus to each field. Default true.
- errorSummaryTitle?: (count: number) => ReactNode = defaultTitle. Heading of the summary. Default "Fix {n} fields to continue".
- children?: ReactNode
When to use:
- Any group of fields submitted together: connect a repository, create a token, invite reviewers.
- errors for server-side validation, keyed by each Field's name.
- FormActions for the button row: primary action last.
When not to use:
- A single setting that saves on change: use Switch or an inline control without a form.
- Search and filter bars that update results live: no submit, no Form.
Do:
- Validate on submit, then live. Do not flag fields while the user is still typing for the first time. After a failed submit, errors clear as soon as the value is fixed.
- Keep what the user typed. Show the error next to the value so it can be corrected, never clear the field.
Don't:
- Disabling submit until the form is valid. A disabled button explains nothing. Keep it enabled and show what is missing on submit.
- One generic error at the top. "Something went wrong" sends users hunting. Attach each error to its field.
Accessibility:
- Base UI Form: native form element; fields validate on submit and the first invalid field gets focus.
- Server errors set aria-invalid on the control and are linked with aria-describedby.
- The error summary is role alert, receives focus when new errors arrive, and links move focus to each field.
- Errors are icon plus text, never color alone.
Tokens used: --border-width-1, --border-width-default, --color-feedback-danger-bg, --color-feedback-danger-border, --color-feedback-danger-icon, --color-feedback-danger-text, --color-text-default, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-line-height-snug, --font-size-200, --font-weight-semibold, --radius-container, --radius-inset, --space-0-5, --space-1, --space-2, --space-3, --space-4, --space-5
Docs facts: [/docs-ai/form.json](/docs-ai/form.json)
### Input
Single-line text field with optional prefix and suffix. Give it a label prop for a complete field, or place it inside a Field.
Import: `import { Input } from '@bridge-ds/react'`
Accepts all native <input> attributes except size, prefix.
Props:
- label?: string. Visible label. When set, the Input wraps itself in a Field (label, hint, error wired for you). Leave it unset when the Input is already inside a Field.
- hint?: ReactNode. Helper text under the label (Field description).
- error?: ReactNode. Error message. Marks the input invalid and announces the message.
- prefix?: ReactNode. Content before the value: an Icon, a currency symbol, a unit. Decorative unless it is text.
- suffix?: ReactNode. Content after the value: a unit, a Kbd hint, a clear IconButton.
- size?: 'sm' | 'md' | 'lg' = 'md'. Height follows the density tokens. Default `md`.
- hideLabel?: boolean. Visually hide the Field label (still the accessible name). Only with `label`.
- onValueChange?: (value: string) => void. Called with the new string value on every change.
When to use:
- Short free text: a name, a URL, a search query, a value with a unit.
- prefix for an icon or a currency, suffix for a unit or a keyboard hint.
- size follows density: md in forms and in a page's filter bar, sm only in dense toolbars, menus and panels, lg only on sparse screens. In a filter bar the search field, its Select and its SegmentedControl share one size, so the row reads as one height.
When not to use:
- More than one line: use Textarea.
- Numbers with steps, min and max: use NumberField (increment buttons, arrow keys, locale formatting).
- Choosing from a known list: Select for under about ten options, Combobox for long or searchable lists.
Do:
- Label above, format help in the hint. The label stays visible while typing; the hint answers the question before it is asked.
- Units in the suffix. The value stays a number the user can edit; the unit is context, not content.
Don't:
- Clearing the value on error. Keep what the user typed and say how to fix it.
Accessibility:
- Base UI Input: inherits label, description and error wiring from the surrounding Field.
- The field edge (color.border.strong) has at least 3:1 contrast with the surface in both themes (WCAG 1.4.11).
- Focus shows a 2px signal-colored edge on every focus, since users type into it.
- On touch screens the text is 17px so iOS does not zoom the page on focus.
- A prefix icon is decorative; if the prefix carries meaning (a currency), make it text.
Tokens used: --border-width-default, --color-bg-subtle, --color-border-default, --color-text-disabled, --color-text-muted, --control-height-lg, --control-height-md, --control-height-sm, --control-padding-x-md, --control-padding-x-sm, --focus-ring-color, --font-family-sans, --font-line-height-normal, --font-size-200, --font-size-300, --input-bg, --input-bg-disabled, --input-border, --input-border-hover, --input-border-invalid, --input-placeholder, --input-radius, --input-text, --motion-duration-instant, --motion-easing-standard, --space-2
Docs facts: [/docs-ai/input.json](/docs-ai/input.json)
### NumberField
Numeric input with decrease and increase buttons, arrow-key stepping, min and max, and locale-aware formatting.
Import: `import { NumberField } from '@bridge-ds/react'`
Props:
- label?: string. Visible label. When set, the NumberField wraps itself in a Field. Leave unset inside a Field.
- hint?: ReactNode
- error?: ReactNode
- value?: number | null
- defaultValue?: number
- onValueChange?: (value: number | null) => void
- min?: number
- max?: number
- step?: number. Amount for the buttons and arrow keys. Shift+arrow uses largeStep. Default 1.
- largeStep?: number
- format?: Intl.NumberFormatOptions. Intl number format, for example `{ style: 'unit', unit: 'pixel' }` or `{ maximumFractionDigits: 2 }`.
- suffix?: ReactNode. Unit or symbol shown after the value inside the box, for example "px".
- hideSteppers?: boolean = false. Hide the increment and decrement buttons (keyboard and wheel still work).
- size?: 'sm' | 'md' | 'lg' = 'md'
- name?: string
- placeholder?: string
- disabled?: boolean
- readOnly?: boolean
- required?: boolean
- hideLabel?: boolean
- id?: string
- className?: string
When to use:
- Exact numbers within known bounds: approvals, columns, spacing steps, retry counts.
- suffix for the unit (px, ms, %), format for currency or percent.
When not to use:
- Identifiers that only look like numbers (zip codes, ids, phone numbers): use Input with inputMode numeric.
- Rough values where position matters: use Slider.
Do:
- Set min and max. Bounds disable the buttons at the edges and stop impossible values.
Don't:
- Unit in the label and in the value. Say it once, in the suffix: "Base spacing" with px, not "Base spacing (px)" plus px.
Accessibility:
- Base UI NumberField: a text input with role textbox and aria-roledescription Number field; Arrow Up and Down step, Shift uses largeStep.
- The decrease and increase buttons are named and disabled at min and max.
- Inherits label, description and error from Field; invalid sets aria-invalid and a danger edge.
- Values use tabular figures so digits do not jump while stepping.
Tokens used: --border-width-default, --color-border-default, --color-surface-hover, --color-surface-pressed, --color-text-default, --color-text-disabled, --color-text-muted, --control-height-lg, --control-height-md, --control-height-sm, --control-padding-x-sm, --focus-ring-color, --focus-ring-width, --font-family-sans, --font-size-200, --font-size-300, --input-bg, --input-bg-disabled, --input-border, --input-border-hover, --input-border-invalid, --input-radius, --input-text, --motion-duration-instant, --motion-easing-standard, --space-2
Docs facts: [/docs-ai/number-field.json](/docs-ai/number-field.json)
### RadioGroup
One choice among a few visible, mutually exclusive options. Arrow keys move the selection; the group has a legend and optional per-option descriptions.
Import: `import { RadioGroup } from '@bridge-ds/react'`
Props:
- label: ReactNode. Group label, rendered as the fieldset legend. Ask the question the options answer.
- children: ReactNode. Radio items.
- description?: ReactNode. Help under the legend.
- error?: ReactNode. Error message for the group. Marks it invalid and is linked with aria-describedby.
- value?: string
- defaultValue?: string
- onValueChange?: (value: string) => void
- orientation?: 'vertical' | 'horizontal' = 'vertical'. Default `vertical`. Use `horizontal` only for two or three short options.
- name?: string
- disabled?: boolean
- readOnly?: boolean
- required?: boolean
- hideLabel?: boolean = false. Visually hide the legend (it stays the accessible name).
- className?: string
When to use:
- Two to about six exclusive options that benefit from being compared side by side.
- Options that need a description to choose well.
- horizontal only for two or three short options, like Light, Dark, System.
When not to use:
- More than about six options, or options users know by name: use Select.
- A single on or off: use Switch or Checkbox.
- Switching views of content: use Tabs.
Do:
- Preselect the safest or most common option. A default saves a decision; pick the one most teams want.
Don't:
- A single radio button. One radio cannot be unselected. Use a Checkbox.
Accessibility:
- Base UI RadioGroup: role radiogroup named by the legend; one tab stop, arrow keys move and select.
- Each radio is labelled by its text (aria-labelledby) and described by its description.
- The group error is linked with aria-describedby and sets aria-invalid; it shows an icon and text.
- Selected uses the ink fill with a light dot; the unselected edge is color.border.strong (3:1).
Tokens used: --border-width-1, --border-width-default, --checkbox-bg, --checkbox-bg-checked, --checkbox-border, --checkbox-fg-checked, --color-action-disabled-bg, --color-action-disabled-text, --color-border-default, --color-border-hover, --color-control-checked-bg-hover, --color-feedback-danger-icon, --color-feedback-danger-text, --color-text-default, --color-text-disabled, --color-text-muted, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-line-height-snug, --font-size-100, --font-size-200, --font-tracking-wide, --font-weight-medium, --input-border-invalid, --motion-duration-fast, --motion-duration-instant, --motion-easing-enter, --motion-easing-standard, --radius-pill, --size-4, --size-6, --space-0-5, --space-1, --space-1-5, --space-2, --space-6
Docs facts: [/docs-ai/radio-group.json](/docs-ai/radio-group.json)
### SegmentedControl
Picks one of a few views or modes, shown side by side as attached buttons: the brand of a preview, the density, light or dark. The choice applies at once, on the current screen.
Import: `import { SegmentedControl, markSegmentOverflow } from '@bridge-ds/react'`
Accepts all native <div> attributes except children, defaultValue, onChange, aria-label, dir.
Props:
- items: SegmentedControlItem[]. The segments, two or more. An array (not children) so the control also renders from Astro and other static templates. When they do not fit (six filters on a phone), the track scrolls sideways inside its container and never widens the page; the selected segment is scrolled into view.
- label?: string. Accessible name of the group. Required unless you pass `aria-labelledby` pointing at a visible label.
- value?: string
- defaultValue?: string
- onValueChange?: (value: string) => void
- size?: 'sm' | 'md' = 'md'. Default `md`. Use `sm` in toolbars and dense panels. Heights follow the density tokens.
- appearance?: 'default' | 'strong' = 'default'. Default `default`. `strong` fills the selected segment with Deep Harbor: one per view at most.
- fullWidth?: boolean = false. Stretch to the container; segments share the width equally.
- disabled?: boolean
- name?: string. Form field name: the selected value is submitted with the form.
When to use:
- Switching how the same content is shown: Light or Dark, Comfortable or Compact, Grid or List.
- Short options where seeing every option at once helps the choice: two to five usually, more for filters people switch between often (status filters). On a phone the track scrolls sideways and never widens the page.
- Appearance strong when the control is what the view is about (the status filter of a list): the selected segment fills with Deep Harbor. At most one strong control per view.
- The change applies immediately, with no Save button.
- Size sm in dense toolbars, menus and panels; md everywhere else, including a page's filter bar, where it matches the md search field and Select beside it. Heights follow the density setting.
- fullWidth inside a narrow panel or menu, so the segments share the width equally.
When not to use:
- Switching between panels of different content: use Tabs, which link each tab to its panel.
- A setting in a form that is saved later: use RadioGroup, which shows a label and help per option.
- Long labels, or options people pick once and forget: use Select.
- Turning one thing on or off: use Switch.
- Triggering actions (Export, Delete): use Button or an attached ButtonGroup.
Do:
- Short, parallel labels. One or two words per segment, same grammatical form, so the set reads as one choice.
- Label outside the track. Put a visible label to the left of the track, as a caption on its center line, and name the group with it (aria-labelledby, or the same words in label). The track holds the options and nothing else.
- Strong for the control the view is about. A list filtered by status: the selected filter fills with Deep Harbor, the strongest signal on the screen. Six short filters are fine; on a phone they scroll sideways.
- Always start with a selection. A segmented control shows the current mode. Pass value or defaultValue so one segment is always selected.
Don't:
- Actions dressed as segments. Segments select a mode; they never run a command. "Export" and "Share" belong on buttons.
- The label inside the track. Never put the group's label in the same pill as the options. Inside one container it reads as one more option ("Brand" next to "Bridge" and "Ocean"). Keep it outside, to the left.
- Too many or too long options. Two or three words per segment make the track longer than the screen and stop reading as one control. Keep labels to one or two words, or use Select.
Accessibility:
- Base UI RadioGroup with native button radios: role radiogroup on the row, role radio and aria-checked on each segment.
- Tab enters the group on the selected segment and leaves it; arrow keys move the selection, Home and End jump to the ends.
- Give the group a name: aria-labelledby pointing at the visible label outside the track, or label (aria-label) when the context already names it.
- The selected segment is marked by a raised plate with a checked-color edge (or the Deep Harbor fill with strong) and a darker text color, not by color alone.
- When the track scrolls, arrow keys still move the selection and the selected segment scrolls into view.
- The focus ring (focus.ring.color) shows on keyboard focus only.
- Rendered without JavaScript, the HTML keeps role, aria-checked and data-value, so a small script can drive it: flip aria-checked and tabindex, and handle arrow keys.
Tokens used: --border-width-default, --color-action-disabled-bg, --color-bg-subtle, --color-border-default, --color-control-checked-bg, --color-control-checked-fg, --color-surface-pressed, --color-surface-raised, --color-text-default, --color-text-disabled, --color-text-muted, --control-height-md, --control-height-sm, --elevation-raised, --focus-ring-color, --focus-ring-width, --font-family-sans, --font-line-height-tight, --font-size-100, --font-size-200, --font-weight-medium, --motion-duration-instant, --motion-easing-standard, --radius-control, --radius-inset, --space-0-5, --space-1-5, --space-2, --space-3, --space-8
Docs facts: [/docs-ai/segmented-control.json](/docs-ai/segmented-control.json)
### Select
Picks one value from a short, known list in a popup. The trigger looks like an Input so forms read as one family; the list supports groups, descriptions, badges and disabled options.
Import: `import { Select } from '@bridge-ds/react'`
Accepts all native <button> attributes except value, defaultValue, onChange, prefix, children.
Props:
- options: SelectOption[]. The choices. Keep them under about ten; use Combobox for long or searchable lists.
- placeholder?: string = 'Select an option'. Text shown when nothing is selected. Describe the choice, not the action: "No theme".
- value?: string | null. Selected value (controlled). `null` means nothing selected.
- defaultValue?: string | null. Initially selected value (uncontrolled).
- onValueChange?: (value: string | null) => void. Called with the new value when the user picks an option.
- label?: string. Visible label. When set, the Select wraps itself in a Field. Leave unset inside a Field.
- hint?: ReactNode. Help under the label (Field description). Only with `label`.
- error?: ReactNode. Error message. Marks the select invalid and announces the message. Only with `label`.
- hideLabel?: boolean. Visually hide the Field label (it stays the accessible name).
- required?: boolean
- name?: string. Name for form submission (a hidden input carries the value).
- size?: 'sm' | 'md' | 'lg' = 'md'. Height follows the density tokens. Default `md`.
- prefix?: ReactNode. Content before the value, usually an Icon.
- open?: boolean. Popup open state (controlled).
- defaultOpen?: boolean. Open on first render (uncontrolled).
- onOpenChange?: (open: boolean) => void
- portalContainer?: HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null. Where the popup is rendered. Default: the end of the body, above every layer including a Dialog or Drawer, so the list is never covered. Pass an element (or a ref) to keep it inside a container, for example a shadow root.
When to use:
- One choice from about 4 to 10 known options: a theme, a brand, a token group, a role.
- The options are long words or labels that would crowd a RadioGroup.
- Space is tight: in a dense toolbar, a menu or a settings row (size sm). In a page's filter bar, size md, like the search field beside it.
- Each option carries a short fact worth seeing before the pick, like a status or a count: put a Badge in badge. It shows on the item and in the trigger once selected.
When not to use:
- Two or three options that should all stay visible: use RadioGroup, or an attached ButtonGroup for a view switch.
- Long or unknown lists (people, repositories, 20+ tokens): use Combobox so users can type to filter.
- Several values at once: use Combobox with multiple, or a CheckboxGroup.
- Actions (Duplicate, Delete): use Menu. A Select changes a value, it does not run commands.
Do:
- A status next to the label with badge. A Badge in the option's badge slot says the status at a glance, in the list and in the trigger. The description stays for a second line of detail, like who made it.
- Order options by meaning, not alphabetically by default. Put the most common choice first, or follow the natural order (System, Light, Dark). Group long lists under short headings.
- Explain a disabled option. A description on the option says why it cannot be picked, instead of leaving users guessing.
Don't:
- A Select with two options. Hiding two choices behind a click costs a step for nothing. Show them as radios or a Switch.
- The placeholder as an instruction. "Click to select" says nothing. The placeholder describes the empty state: "No theme", "Any reviewer".
Accessibility:
- Base UI Select: the trigger is a combobox button with aria-expanded; the popup is a listbox with options marked aria-selected and aria-disabled.
- Keyboard: Enter, Space or Arrow keys open it; arrows move; typing jumps to a matching option; Enter selects; Escape closes and returns focus to the trigger.
- The Field label names the trigger; hint and error are linked through aria-describedby, and an error sets aria-invalid.
- The selected option shows a check mark and a medium weight, not color alone.
- The trigger edge uses color.border.strong (3:1 against surfaces) and the signal focus ring on keyboard focus.
- The popup is portaled and re-applies the theme, brand and density of its trigger.
Tokens used: --border-width-default, --color-bg-subtle, --color-border-default, --color-surface-hover-raised, --color-text-default, --color-text-disabled, --color-text-muted, --control-height-lg, --control-height-md, --control-height-sm, --control-padding-x-md, --control-padding-x-sm, --focus-ring-color, --font-family-sans, --font-line-height-normal, --font-line-height-snug, --font-size-100, --font-size-200, --font-size-300, --font-tracking-wide, --font-weight-medium, --input-bg, --input-bg-disabled, --input-border, --input-border-hover, --input-border-invalid, --input-placeholder, --input-radius, --input-text, --layer-popover, --menu-item-height, --motion-duration-fast, --motion-duration-instant, --motion-easing-enter, --motion-easing-exit, --motion-easing-standard, --popup-bg, --popup-border, --popup-radius, --popup-shadow, --size-96, --space-1, --space-2
Docs facts: [/docs-ai/select.json](/docs-ai/select.json)
### Slider
Picks a value or a range by position on a continuous scale, with the formatted value shown at the end of the track.
Import: `import { Slider } from '@bridge-ds/react'`
Props:
- label?: string. Visible label. When set, the Slider wraps itself in a Field. Leave unset inside a Field.
- hint?: ReactNode
- error?: ReactNode
- value?: number | number[]. A number for one thumb, an array of two numbers for a range.
- defaultValue?: number | number[]
- onValueChange?: (value: number | number[]) => void. Fires while dragging.
- onValueCommitted?: (value: number | number[]) => void. Fires once when the user releases the thumb or stops pressing keys. Use it for expensive work.
- min?: number
- max?: number
- step?: number
- largeStep?: number. Step for Page Up, Page Down and Shift+arrow. Default 10.
- format?: Intl.NumberFormatOptions. Intl format for the displayed value, for example `{ style: 'percent' }`.
- showValue?: boolean = true. Show the formatted value at the end of the track. Default true.
- thumbLabels?: string[]. Accessible names of the thumbs when there is no visible label, or to name range ends.
- name?: string
- disabled?: boolean
- hideLabel?: boolean
- className?: string
When to use:
- Approximate values where the position matters more than the exact number: opacity, threshold, zoom.
- A range with two thumbs: pass an array of two numbers.
- Live previews: update on onValueChange, save on onValueCommitted.
When not to use:
- An exact number the user knows: use NumberField.
- Very large ranges where one step is tiny: pair it with a NumberField or use NumberField alone.
Do:
- Show the value and its unit. format makes the value readable: 48%, 4.5, 16 px.
Don't:
- A slider for precise input. Dragging to exactly 37 is frustrating. Use NumberField when the exact number matters.
Accessibility:
- Base UI Slider: each thumb is a native range input with role slider, aria-valuenow and aria-valuetext from format.
- Arrow keys step, Page Up and Page Down or Shift+arrow use largeStep, Home and End jump to min and max.
- Range thumbs are named "<label> minimum" and "<label> maximum"; override with thumbLabels.
- The filled range uses the signal color (3:1 on the muted track); focus rings the thumb.
Tokens used: --border-width-strong, --color-border-strong, --color-text-default, --color-text-disabled, --elevation-raised, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-sans, --font-size-200, --input-border-invalid, --motion-duration-instant, --motion-easing-standard, --radius-pill, --size-20, --size-4, --size-6, --slider-range, --slider-thumb, --slider-track, --space-1, --space-4
Docs facts: [/docs-ai/slider.json](/docs-ai/slider.json)
### Switch
Turns a setting on or off with immediate effect, like a light switch. No save button needed.
Import: `import { Switch } from '@bridge-ds/react'`
Props:
- label: ReactNode. Visible label. Name the setting, not the state: "Email notifications", not "On".
- description?: ReactNode. Secondary text under the label, linked with aria-describedby.
- checked?: boolean
- defaultChecked?: boolean
- onCheckedChange?: (checked: boolean) => void
- labelPlacement?: 'end' | 'start' = 'end'. `end` puts the label after the switch; `start` puts it first and pushes the switch to the row end (settings lists). Default `end`.
- size?: 'sm' | 'md' = 'md'. Default `md` (36 by 20). `sm` (28 by 16) for dense tables.
- name?: string
- value?: string
- disabled?: boolean
- readOnly?: boolean
- required?: boolean
- id?: string
- className?: string
When to use:
- Settings that apply as soon as they are flipped: notifications, auto-publish, preview mode.
- labelPlacement start in settings lists, so labels align left and switches align right.
When not to use:
- A choice saved later with a form button: use Checkbox.
- More than two states: use RadioGroup or Select.
- Starting an action (Publish, Sync): use Button.
Do:
- Name the setting, not the state. The switch shows on or off; the label says what is on or off.
Don't:
- Labels that change with the state. "On" and "Off" labels repeat what the control already shows and hide what it controls.
- A switch that waits for Save. Users expect a switch to act at once. Inside a form that saves later, use a Checkbox.
Accessibility:
- Base UI Switch: role switch with aria-checked; Space and Enter toggle it; the label row is the click target.
- The off track uses control.track (3:1 against surfaces) and the thumb keeps 3:1 against the track, so the state is visible without color.
- The thumb position carries the state, not only the track color.
- If flipping it fails (network), revert it and say why next to it.
Tokens used: --color-bg-muted, --color-border-hover, --color-border-strong, --color-control-checked-bg-hover, --color-text-default, --color-text-disabled, --color-text-muted, --elevation-raised, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-size-100, --font-size-200, --font-tracking-wide, --motion-duration-fast, --motion-easing-standard, --radius-pill, --size-3, --size-4, --size-5, --size-6, --size-7, --size-9, --space-0-5, --space-3, --switch-thumb, --switch-thumb-on, --switch-track-off, --switch-track-on
Docs facts: [/docs-ai/switch.json](/docs-ai/switch.json)
### Textarea
Multi-line text that grows with its content up to a maximum, with an optional character count. Give it a label prop or place it inside a Field.
Import: `import { Textarea } from '@bridge-ds/react'`
Accepts all native <HTMLTextAreaElement> attributes.
Props:
- label?: string. Visible label. When set, the Textarea wraps itself in a Field. Leave unset inside a Field.
- hint?: ReactNode. Helper text under the label.
- error?: ReactNode. Error message. Marks the textarea invalid and announces the message.
- appearance?: 'field' | 'inline' = 'field'. `field` (default) is the boxed form control. `inline` edits existing text in place: no border, no resize handle, the surrounding font, size and line height, and a height that always fits the content (`rows`, `maxRows` and `autosize` are ignored). Never use `inline` in a form.
- rows?: number = 3. Minimum visible lines. Default 3. Ignored by `appearance="inline"`, which always fits its content.
- maxRows?: number = 12. Grow with the content up to this many lines, then scroll. Default 12. Ignored by `appearance="inline"`.
- autosize?: boolean = true. Grow with the content. Default true. When false the user can resize vertically. Ignored by `appearance="inline"`.
- showCount?: boolean = false. Show "used / maxLength" under the field. Needs maxLength.
- hideLabel?: boolean. Visually hide the Field label. Only with `label`.
- onValueChange?: (value: string) => void. Called with the new string value on every change.
When to use:
- Free text longer than one line: pull request summaries, release notes, review comments.
- showCount with maxLength when there is a hard limit.
- appearance="inline" only to edit existing text in place, where the text is the page: a paragraph, a list item or a card title in a docs editor. It keeps the surrounding font, size and line height and shows a field on hover and focus.
When not to use:
- A short single value: use Input.
- Structured content (lists of tokens, key-value pairs): use dedicated fields.
- appearance="inline" in a form, a dialog or a settings page: people scan forms for boxes, and an inline field there reads as plain text. Use the default field.
Do:
- Size it for the expected answer. rows sets the starting height; autosize grows it, so short boxes do not hide long answers.
- Inline only where the text already is. Swap the paragraph or list item for an inline Textarea holding the same text, so the page keeps its layout while it is edited. Name each one with aria-label.
Don't:
- Hiding the limit until it is hit. Show the count while typing so users can edit before the error.
- Inline fields in a form. Without a box, an empty form field reads as finished text. Forms use the default field with a visible label.
Accessibility:
- Native textarea through Base UI Field: label, description and error are wired for you.
- Focus shows a 2px signal edge; invalid sets aria-invalid and a danger edge with a text error.
- The visual counter is hidden from screen readers to avoid announcing every keystroke; maxLength is enforced natively.
- On touch screens the text is 17px so iOS does not zoom on focus.
- appearance="inline" is still a native, labelled textarea. Hover adds a fill and a hairline edge and focus the 2px DS ring, so the field never relies on color alone.
- The inline placeholder uses the muted text color, checked at 4.5:1 on the canvas, surfaces and the hover fill.
- Inline text inherits the page size, also on touch screens: keep the surrounding text at 16px or more so iOS does not zoom on focus.
Tokens used: --border-width-default, --color-bg-subtle, --color-border-default, --color-surface-hover, --color-text-disabled, --color-text-muted, --control-padding-x-md, --focus-ring-color, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-size-100, --font-size-200, --font-size-300, --font-tracking-wide, --input-bg, --input-bg-disabled, --input-border, --input-border-hover, --input-border-invalid, --input-placeholder, --input-radius, --input-text, --motion-duration-instant, --motion-easing-standard, --radius-inset, --space-0-5, --space-1, --space-2
Docs facts: [/docs-ai/textarea.json](/docs-ai/textarea.json)
### Alert
An inline message about the state of a page or a section: a failed publish, a pending review, a sync in progress. It stays until the situation changes or the user dismisses it.
Import: `import { Alert } from '@bridge-ds/react'`
Accepts all native <div> attributes except title.
Props:
- tone?: 'neutral' | 'info' | 'success' | 'warning' | 'danger' = 'info'. Meaning of the message. The icon and the text carry it too, never the color alone. Default `info`.
- title?: ReactNode. Short summary in bold. Optional when the description is one short sentence.
- icon?: LucideIcon | false. Replace the tone icon, or pass `false` to hide it.
- actions?: ReactNode. Buttons or links, for example "Review changes". Keep to two.
- actionsPlacement?: 'below' | 'inline' = 'below'. `below` (default): actions sit under the text. `inline`: on the same line as a short message, at its end, and under it when the line is too narrow (a phone). Use size sm buttons inline.
- onDismiss?: () => void. Shows a dismiss button and calls this when it is pressed.
- dismissLabel?: string = 'Dismiss'. Accessible name of the dismiss button. Default "Dismiss".
- announce?: 'auto' | 'off' = 'auto'. How assistive tech hears the alert when it appears after page load. `auto` uses role="alert" (interrupts) for danger and warning and role="status" (polite) for the others; `off` renders static content with no live region, right for alerts present when the page loads. Default `auto`.
When to use:
- A condition that affects what the user is doing here: a failed build, a protected branch, contrast warnings.
- Tone follows meaning: danger for failed or blocked, warning for needs attention, success for a finished task that changed this page, info for context, neutral when there is no judgement.
- Inside a form or card, above the content it is about.
- actionsPlacement inline for one short message and one action, like a banner saying which change you are viewing with Back to latest at the end of the line. It wraps under the text on a phone. Keep below for a title plus a longer description.
When not to use:
- Confirming an action the user just took (Copied, Saved): use a Toast.
- An error on one field: use the Field error under that control.
- A decision the user must make before continuing: use an AlertDialog.
Do:
- Title says what happened, description says what to do. Users scan the bold line and read on only when they need to act.
- Give the way out as an action. A button next to the problem saves a search through the page.
Don't:
- Stacking several alerts. Three alerts in a row read as noise. Merge them, or keep the most severe and link to the rest.
- Danger for something that is not broken. Red trains people to ignore red. Use info or warning when nothing failed.
Accessibility:
- Alerts that appear after load are announced: danger and warning with role="alert" (interrupts), the others with role="status" (polite).
- Alerts present when the page loads should use announce="off": a live region on load is either ignored or read twice.
- Every tone has its own icon and the text carries the meaning, so color is never the only signal (WCAG 1.4.1).
- The dismiss button is a named IconButton (dismissLabel, default "Dismiss"). Move focus to a sensible place after dismissing if the alert had focus.
- Feedback text and icon colors are checked against their tinted backgrounds in both themes.
Tokens used: --alert-radius, --border-width-default, --color-bg-subtle, --color-border-default, --color-feedback-danger-bg, --color-feedback-danger-border, --color-feedback-danger-icon, --color-feedback-info-bg, --color-feedback-info-border, --color-feedback-info-icon, --color-feedback-success-bg, --color-feedback-success-border, --color-feedback-success-icon, --color-feedback-warning-bg, --color-feedback-warning-border, --color-feedback-warning-icon, --color-text-default, --color-text-muted, --control-height-sm, --font-family-sans, --font-line-height-normal, --font-size-200, --font-weight-semibold, --space-1, --space-2, --space-3, --space-4
Docs facts: [/docs-ai/alert.json](/docs-ai/alert.json)
### Banner
A page-level mode banner: the page shows something other than usual, such as a past change of the library or an edit mode, and the way back sits on the same line. One per page, at the top of the content.
Import: `import { Banner } from '@bridge-ds/react'`
Accepts all native <div> attributes except title.
Props:
- title: ReactNode. The mode, in one sentence: "Viewing the library as change #118. Nothing is saved."
- children?: ReactNode. One more line when the mode needs it: what is different, what is kept.
- icon?: LucideIcon. Leading Lucide icon for the mode, for example Eye for a preview.
- actions?: ReactNode. The way out, and at most one more action, on the same line: Buttons with `variant="secondary"`.
- appearance?: 'strong' | 'neutral' = 'strong'. `strong` (default): Deep Harbor, the one strong color, for a mode the whole page is in. `neutral`: the subtle ground, for a quieter mode such as a scoped editor.
- sticky?: 'top' | 'below-header'. Keep the banner in view while the page scrolls: at the top of the scroll area, or under the app header (`layout.header.height`).
When to use:
- The whole page is in a mode the reader must not forget: previewing a past change, editing guidelines, viewing as another brand.
- Give the way out as an action on the same line ("Back to latest library"), a Button with size sm and variant secondary, and at most one more action. On narrow screens the actions go under the text and fill the row.
- strong (default, Deep Harbor) for a mode the whole page is in; neutral for a quieter, scoped mode such as an editor.
- sticky="top" or sticky="below-header" when the page scrolls long, so the way back stays in view.
When not to use:
- A message about the page's content (a failed build, a contrast warning): use Alert.
- Confirming an action the user just took: use a Toast.
- Marketing or announcements: Banner is for a state the page is in, not for news.
- More than one per page: merge the modes into one sentence, or pick the one that matters.
Do:
- Say the mode and its consequence in one sentence. What the reader sees and what it means for their work: "Viewing the library as change #118. Nothing is saved."
- Neutral for a scoped mode. An edit mode on one part of the page does not need the strong color. Keep Deep Harbor for modes the whole page is in.
Don't:
- A banner for news. "New components are available" is not a mode. It belongs in a changelog or an Alert, not in the strong band at the top of the page.
- A vague mode. "Preview mode" does not say what is previewed or whether work is saved. Name the object and the consequence.
Accessibility:
- A landmark region named by its title (role region, aria-labelledby), so screen reader users find the mode in the landmarks list.
- Not a live region: the banner is part of the page when it loads. If a mode starts without a page load, move focus to the banner title or announce the mode once.
- The actions are real Buttons on the same line as the title, reached with Tab right after the title.
- The strong appearance uses color.surface.strong (Deep Harbor in both themes) with color.text.on-solid, 8.87:1, and the focus ring inside it takes the text color. The icon and the words carry the mode, never color alone.
- Sticky banners stay under the app header (layout.header.height) and above the content (layer.sticky), so they never hide the header's controls.
Tokens used: --border-width-default, --color-bg-subtle, --color-border-default, --color-surface-strong, --color-text-default, --color-text-muted, --color-text-on-solid, --control-height-md, --font-family-sans, --font-weight-semibold, --icon-size-md, --layer-sticky, --layout-header-height, --radius-container, --size-60, --space-0-5, --space-2, --space-3, --space-4, --typography-ui-line-height, --typography-ui-size
Docs facts: [/docs-ai/banner.json](/docs-ai/banner.json)
### EmptyState
Explains why a view has no content yet and invites the action that fills it. An empty screen is a starting point, not a dead end.
Import: `import { EmptyState } from '@bridge-ds/react'`
Accepts all native <div> attributes except title.
Props:
- title: ReactNode. What is empty, said plainly: "No pull requests yet".
- icon?: LucideIcon. A Lucide icon that names the object that is missing (GitPullRequest for pull requests).
- description?: ReactNode. Why it is empty and what fills it. One or two sentences.
- actions?: ReactNode. The next step: usually one primary Button, plus an optional secondary Button or Link.
- size?: 'sm' | 'md' | 'lg' = 'md'. `sm` inside a card or table, `md` in a panel, `lg` for a whole page. Default `md`.
- headingLevel?: 2 | 3 | 4 | 5 | 6 = 3. Heading level of the title, to fit the page outline. Default 3.
- align?: 'center' | 'start' = 'center'. `center` for pages and panels, `start` inside left-aligned layouts. Default `center`.
When to use:
- First use: nothing has been created yet (no tokens, no pull requests). Explain the value and offer the first step.
- No results: a search or filter matched nothing. Say what was searched and how to widen it.
- Cleared: the user finished everything (no reviews waiting). Confirm it calmly.
- size sm inside tables and cards, md in panels, lg for a whole page.
When not to use:
- Errors that stopped content from loading: use an Alert with a retry action.
- Content still loading: use Skeleton.
Do:
- Say what goes here and how to get it. The title names the missing object, the description says where it comes from, the button starts it.
Don't:
- "Nothing here" with no way forward. It describes the screen the user already sees and leaves them stuck.
Accessibility:
- The title is a real heading (headingLevel, default h3) so it appears in the page outline; pick the level that fits the page.
- The icon is decorative and hidden from assistive tech: the title carries the meaning.
- Actions are regular Buttons or Links in the tab order, after the text.
Tokens used: --border-width-default, --color-border-default, --color-surface-default, --color-text-default, --color-text-muted, --font-family-sans, --font-line-height-normal, --font-line-height-snug, --font-line-height-tight, --font-size-200, --font-size-300, --font-size-400, --font-size-600, --font-tracking-snug, --font-tracking-tight, --font-weight-semibold, --radius-container, --radius-control, --size-10, --size-12, --size-14, --space-1, --space-1-5, --space-10, --space-16, --space-2, --space-3, --space-4, --space-5, --space-6
Docs facts: [/docs-ai/empty-state.json](/docs-ai/empty-state.json)
### Progress
Shows how far a task has gone: a linear bar in layouts, a circular ring in compact spots. Indeterminate when the duration is unknown.
Import: `import { Progress } from '@bridge-ds/react'`
Accepts all native <div> attributes except children.
Props:
- value: number | null. Current value. `null` means indeterminate (the duration is unknown).
- min?: number = 0
- max?: number = 100
- label?: ReactNode. Visible label, for example "Publishing tokens". Without it, pass aria-label.
- showValue?: boolean. Show the formatted value ("64%") next to the label, or inside a large ring. Default true for linear with a label.
- variant?: 'linear' | 'circular' = 'linear'. `linear` bar for tasks in a layout; `circular` ring for compact spots. Default `linear`.
- size?: 'sm' | 'md' | 'lg' = 'md'. Bar thickness or ring diameter. Default `md`.
- tone?: 'signal' | 'success' | 'warning' | 'danger' = 'signal'. Signal color while working; switch to success or danger when the task ends. Default `signal`.
- format?: Intl.NumberFormatOptions. Intl number format for the value text. Default: percent of the range.
- getAriaValueText?: (formattedValue: string, value: number | null) => string. Human-readable value for screen readers, for example "12 of 40 tokens".
When to use:
- Tasks that take more than about two seconds and report progress: publishing, importing from Figma, running contrast checks.
- Indeterminate (value null) when the task runs but its length is unknown.
- Switch tone to success or danger when the task ends, and say so in the label.
When not to use:
- Short waits or busy controls: use Spinner (or Button loading).
- Content that is loading into a layout: use Skeleton in the shape of the content.
- A static measurement like storage used or a score: that is a meter, not progress.
Do:
- Label the task, show the value. "Publishing tokens 64%" tells users what is happening and how long is left.
- Use counts when they mean more than percent. getAriaValueText and the label can say "12 of 40 pairs".
Don't:
- A progress bar that never moves. If you cannot report progress, use the indeterminate state rather than a fake value.
Accessibility:
- Base UI Progress: role="progressbar" with aria-valuenow, aria-valuemin and aria-valuemax; indeterminate drops aria-valuenow.
- Needs a name: the label prop, or aria-label when there is no visible label (rings).
- The visible value text is aria-hidden; screen readers get it from aria-valuetext (customize with getAriaValueText).
- The fill uses the signal color with at least 3:1 against the track and surfaces.
- Under reduced motion the indeterminate bar stops sliding and shows a full, softened track instead.
Tokens used: --color-feedback-danger-icon, --color-feedback-success-icon, --color-feedback-warning-icon, --color-text-default, --color-text-muted, --font-family-sans, --font-line-height-normal, --font-size-100, --font-size-200, --font-weight-medium, --motion-duration-moderate, --motion-duration-shimmer, --motion-duration-spin, --motion-easing-standard, --progress-fill, --progress-track, --radius-pill, --size-14, --size-5, --size-8, --space-1, --space-1-5, --space-2, --space-3
Docs facts: [/docs-ai/progress.json](/docs-ai/progress.json)
### Skeleton
A placeholder in the shape of content that is loading, so the layout does not jump when the data arrives.
Import: `import { Skeleton } from '@bridge-ds/react'`
Accepts all native <span> attributes.
Props:
- variant?: 'text' | 'circle' | 'rect' = 'text'. `text` draws lines at the current font size; `circle` an avatar; `rect` a block (thumbnail, chart, card). Default `text`.
- lines?: number = 1. Number of text lines. The last one is shorter, like a real paragraph. Default 1.
- width?: string. CSS width, for example `"12ch"` or `"100%"`. Defaults: text 100%, circle 40px, rect 100%.
- height?: string. CSS height for `rect` and `circle`.
- static?: boolean. Stop the shimmer (for example in long lists). Reduced motion stops it everywhere.
When to use:
- First load of a list, table, card or detail panel whose shape you know.
- Mirror the real layout: a circle for an avatar, short lines for names, a block for a preview.
- Pair with aria-busy="true" on the region and a visually hidden "Loading tokens".
When not to use:
- Inside a button or after a click on a control: use Button loading or Spinner.
- Long tasks with known progress: use Progress.
- When loading takes under about 300 ms: show nothing, the flash is worse than the wait.
Do:
- Match the shape of the real content. The page settles in place instead of reflowing when data lands.
Don't:
- A whole page of shimmering blocks. Load the frame (navigation, titles) for real and use skeletons only where the data goes.
Accessibility:
- Skeletons are always aria-hidden: they are decoration, not content.
- Put aria-busy="true" on the loading region and a VisuallyHidden label like "Loading tokens"; remove both when the content arrives.
- The shimmer stops under prefers-reduced-motion; use static for long lists.
Tokens used: --motion-duration-shimmer, --motion-easing-standard, --radius-container, --radius-pill, --size-10, --size-20, --skeleton-base, --skeleton-radius, --skeleton-shine
Docs facts: [/docs-ai/skeleton.json](/docs-ai/skeleton.json)
### Spinner
An indeterminate activity indicator for short waits and busy controls. For content that is loading into a layout, use Skeleton; for long tasks, use Progress.
Import: `import { Spinner } from '@bridge-ds/react'`
Accepts all native <span> attributes.
Props:
- size?: 'xs' | 'sm' | 'md' | 'lg' = 'sm'. Token size: xs 12, sm 16, md 20, lg 24. Default `sm`.
- tone?: 'current' | 'signal' = 'current'. `current` inherits the text color (inside buttons); `signal` uses the signal color. Default `current`.
- label?: string. Accessible label announced as a status, for example "Loading tokens". Omit when the spinner sits inside a control that already exposes aria-busy: it is then hidden from assistive tech.
When to use:
- Inside a control that is working: Button and IconButton show it automatically with loading.
- Small regions that refresh in place, for waits up to a few seconds.
- tone signal when it stands alone, current when it sits in text or a control.
When not to use:
- First load of a list or a page: use Skeleton in the shape of the content.
- Tasks with measurable progress or that take longer than a few seconds: use Progress.
Do:
- Name what is loading. A standalone spinner needs a label so screen readers hear "Loading tokens".
Don't:
- A spinner in the middle of an empty page. It hides the layout and makes the wait feel longer. Show the frame and use skeletons.
Accessibility:
- With label it renders role="status" and is announced; without label it is aria-hidden (use it inside a control that sets aria-busy).
- Under reduced motion it keeps turning, slower: a frozen spinner looks like a crash.
- The arc is drawn in currentColor or the signal color, which keeps 3:1 against surfaces.
Tokens used: --color-signal-default, --icon-size-lg, --icon-size-md, --icon-size-sm, --icon-size-xs, --motion-duration-spin
Docs facts: [/docs-ai/spinner.json](/docs-ai/spinner.json)
### Toast
A short, temporary notification that confirms what just happened (Tokens published, Link copied) without interrupting. Shown with useToast() under a ToastProvider.
Import: `import { Toast, useToast, ToastProvider } from '@bridge-ds/react'`
Props:
- title: ReactNode. One short line: what happened. "Tokens published".
- description?: ReactNode. Optional detail: what changed or what to do next.
- tone?: 'neutral' | 'info' | 'success' | 'warning' | 'danger' | 'loading'. Icon and meaning. Default `neutral`. `loading` shows a spinner and never times out.
- action?: ToastAction. One action, for example Undo. Toasts with an action stay until dismissed or acted on.
- timeout?: number. Milliseconds before auto dismiss. `0` keeps it until closed. Default 5000 (0 with an action or for loading).
- priority?: 'low' | 'high'. `high` interrupts screen readers (assertive). Default `high` for danger, `low` otherwise.
- id?: string. Reuse an id to update a toast in place.
- onClose?: () => void. Called when the toast closes (timeout, swipe, close button or action).
When to use:
- Confirming an action the user just took, especially when its result is not visible on screen: Copied, Published, Invitation sent.
- Offering a quick reversal: Token deleted, with Undo.
- Background work that finishes later: toast.promise() shows loading, then success or error.
When not to use:
- Information the user must read or act on to continue: use an inline Alert or an AlertDialog.
- Errors in a form: show them next to the fields.
- Anything that needs more than one action or a long explanation.
Do:
- Use the verb of the action, in the past tense. The button said Publish, the toast says Published. The vocabulary follows the flow.
- Offer Undo for destructive actions. A toast with an action stays until the user acts or dismisses it, so nobody misses the way back.
Don't:
- A toast for every click. If the result is visible on screen, the screen already confirms it.
Accessibility:
- Toasts render in a labelled region ("Notifications") that users reach with F6. Hovering or focusing the stack expands it and pauses auto dismiss.
- Titles and descriptions are announced through a live region: politely by default, assertively for danger (priority high).
- Toasts with an action, loading toasts and danger toasts from promise() do not auto dismiss, so screen reader and keyboard users have time to act.
- Close and action buttons are real buttons with names; the close button is labelled "Dismiss notification".
- Swipe right or down dismisses on touch. Under reduced motion toasts appear and leave without sliding.
Tokens used: --border-width-default, --color-feedback-danger-icon, --color-feedback-info-icon, --color-feedback-success-icon, --color-feedback-warning-icon, --color-text-default, --color-text-muted, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-size-200, --font-weight-semibold, --layer-toast, --motion-duration-moderate, --motion-duration-slow, --motion-easing-enter, --motion-easing-exit, --motion-easing-standard, --size-96, --space-0-5, --space-1-5, --space-2, --space-3, --space-4, --space-6, --toast-bg, --toast-border, --toast-radius, --toast-shadow
Docs facts: [/docs-ai/toast.json](/docs-ai/toast.json)
### AlertDialog
Stops the user to confirm an action with consequences, usually destructive: delete tokens, revoke access, force publish. It asks one question and offers two answers.
Import: `import { AlertDialog } from '@bridge-ds/react'`
Props:
- open?: boolean
- defaultOpen?: boolean
- onOpenChange?: (open: boolean) => void
- children?: ReactNode. AlertDialogTrigger and AlertDialogContent.
When to use:
- Before an action that cannot be undone or that affects other people: deleting, revoking, overwriting.
- tone primary for consequential but safe actions, like publishing a release to every consumer.
When not to use:
- Actions that can be undone: do them right away and offer Undo in a Toast.
- Collecting input: use Dialog.
- Routine saves: confirmation fatigue teaches people to click through without reading.
Do:
- Ask a question that names the action and the object. "Delete 3 tokens?" with a confirm button "Delete tokens" is understood without reading the description.
- Say the consequence, including what cannot be undone. "Components that use them fall back to their defaults. This cannot be undone."
Don't:
- "Are you sure?" with Yes and No. It hides what is about to happen. Name the action in the title and on the button.
Accessibility:
- Base UI AlertDialog: role alertdialog, named by the title, described by the consequence.
- Focus starts on Cancel, the safe choice, so a reflexive Enter does not destroy anything.
- It does not close on an outside click; Escape and Cancel close it and focus returns to the trigger.
- While loading, the confirm button keeps focus and announces aria-busy.
Tokens used: --layout-inset, --space-2, --space-3
Docs facts: [/docs-ai/alert-dialog.json](/docs-ai/alert-dialog.json)
### Dialog
A modal window for a short, focused task that must be finished or cancelled before the user goes back to the page: rename a token, invite a reviewer, review a release.
Import: `import { Dialog } from '@bridge-ds/react'`
Props:
- open?: boolean. Controlled open state.
- defaultOpen?: boolean. Initial open state when uncontrolled.
- onOpenChange?: (open: boolean) => void. Called when the dialog opens or closes (Escape, scrim click, close button, DialogClose).
- disablePointerDismissal?: boolean = false. Keep the dialog open when the user clicks the scrim. Use for forms with unsaved input.
- children?: ReactNode. DialogTrigger and DialogContent.
When to use:
- A task of one to three fields that belongs to the current page and needs the user's full attention.
- Reviewing something before committing it, like release notes before Publish.
- Size sm for one field, md (default) for short forms, lg for reading or comparing content.
- Plain-script pages without React render it with dialogHtml() from @bridge-ds/react/dom and call bindDialogs() once: the same look, focus trap, Escape, scrim and focus return. Never a homemade modal or a restyled native dialog.
When not to use:
- Confirming a destructive action: use AlertDialog, which cannot be dismissed by clicking outside.
- Details the user wants to read next to the page, like a token's history: use Drawer.
- A hint or a small menu anchored to a control: use Popover or Menu.
- Long multi-step flows: give them a page. Stacked dialogs lose people.
Do:
- Title says the task, the primary button repeats the verb. "Rename token" opens a dialog titled "Rename token" whose primary action is "Rename token". Users always know where they are.
- Keep the footer for actions only. Alternatives first, primary last, aligned to the end. Explanations go in the description or the body.
Don't:
- A dialog for information the page could show inline. Interrupting to say "Saved" costs a click. Use a Toast or an inline Alert.
- Vague footer labels. "OK" and "Yes" make users reread the title to know what they agree to.
Accessibility:
- Base UI Dialog: role dialog, aria-modal, named by the title and described by the description.
- Focus moves into the dialog when it opens (the first focusable element, or initialFocus) and is trapped there.
- Escape, the close button and a click on the scrim close it; focus returns to the trigger.
- The page behind is inert and scroll is locked while the dialog is open.
- The body scrolls on its own when content is long, so the title and the actions stay visible.
- Motion is a 200ms fade and a slight rise; with reduced motion it appears instantly.
Tokens used: --border-width-default, --color-bg-subtle, --color-border-default, --color-text-default, --color-text-muted, --control-height-sm, --dialog-bg, --dialog-radius, --dialog-scrim, --dialog-shadow, --font-family-sans, --font-line-height-normal, --font-size-200, --layer-modal, --layer-overlay, --layout-inset, --motion-duration-fast, --motion-duration-moderate, --motion-easing-enter, --motion-easing-exit, --motion-easing-standard, --size-140, --size-180, --size-96, --space-1, --space-2, --space-3, --space-4, --space-6, --typography-h3-family, --typography-h3-line-height, --typography-h3-size, --typography-h3-tracking, --typography-h3-weight
Docs facts: [/docs-ai/dialog.json](/docs-ai/dialog.json)
### Drawer
A sheet that slides in from an edge and keeps the page in view behind it: token details, filters, a review checklist. On touch it can be swiped away.
Import: `import { Drawer } from '@bridge-ds/react'`
Props:
- open?: boolean
- defaultOpen?: boolean
- onOpenChange?: (open: boolean) => void
- side?: 'start' | 'end' | 'bottom' = 'end'. Edge the sheet slides from. `end` (default) for details and filters, `start` for navigation, `bottom` on small screens.
- disablePointerDismissal?: boolean = false. Keep it open when the scrim is clicked.
- children?: ReactNode. DrawerTrigger and DrawerContent.
When to use:
- Details or editing for one item of a list or table, where the user wants to keep the list in mind.
- Filters and settings for the current view.
- side end (default) for details, start for navigation, bottom for small screens and quick choices.
When not to use:
- A short task that needs full attention: use Dialog.
- Content that should stay visible while the user works on the page: use a non-modal side panel in the layout.
Do:
- Title it with the item's name. "color.signal.default" tells the user which row they opened. The description says what it is for.
- Put the actions in the footer. The footer stays visible while the body scrolls.
Don't:
- A drawer that opens another drawer. Stacked sheets hide where the user came from. Navigate inside the drawer or use a page.
Accessibility:
- Base UI Drawer: modal dialog semantics, named by the title, focus trapped, Escape closes, focus returns to the trigger.
- Swipe to dismiss is an extra for touch, never the only way: the close button and Escape always work.
- The page behind is inert while the drawer is open.
- When the body is taller than the sheet it becomes a focusable region named by the title, so keyboard users can Tab to it and scroll with the arrow keys. A body that fits adds no tab stop.
- Slides in 300ms with an exponential ease-out; reduced motion shows it instantly.
- start and end follow the physical left and right in v1; RTL layouts should pass the mirrored side.
Tokens used: --border-width-default, --color-bg-subtle, --color-border-default, --color-border-strong, --color-text-default, --color-text-muted, --dialog-scrim, --drawer-bg, --drawer-border, --drawer-radius, --drawer-shadow, --focus-ring-color, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-size-200, --layer-modal, --layer-overlay, --layout-inset, --motion-duration-instant, --motion-duration-moderate, --motion-duration-slow, --motion-easing-enter, --motion-easing-exit, --radius-pill, --size-10, --size-120, --size-160, --size-80, --space-1, --space-12, --space-2, --space-24, --space-3, --typography-h3-family, --typography-h3-line-height, --typography-h3-size, --typography-h3-tracking, --typography-h3-weight
Docs facts: [/docs-ai/drawer.json](/docs-ai/drawer.json)
### Menu
A list of actions or options that opens from a button: row overflow actions, view options, sort order. Supports icons, shortcuts, groups, checkable items and submenus.
Import: `import { Menu, MenuRadioGroup, MenuGroup, MenuSeparator, MenuSubmenu } from '@bridge-ds/react'`
Props:
- open?: boolean
- defaultOpen?: boolean
- onOpenChange?: (open: boolean) => void
- loopFocus?: boolean = true. Wrap focus from the last item to the first. Default true.
- children?: ReactNode. MenuTrigger and MenuContent.
When to use:
- More than two or three actions on one item: show the main one as a button and the rest in a Menu.
- View options: checkable items for columns, a radio group for sort order.
- A submenu for one level of grouping (Move to group, then the groups).
- Leading icons when they help scanning (Rename, Duplicate, Delete): pass icon, a Lucide icon or an element like a brand mark. Put icons on every item of the menu or on none; labels stay aligned either way.
When not to use:
- Choosing a value for a form field: use Select or Combobox, which work with Field and forms.
- Navigation between pages: use links in the sidebar or tabs.
- Two actions: show both as buttons.
Do:
- Destructive items last, after a separator. Distance from routine actions prevents slips. The danger tone backs up the verb.
- Icons on all items or on none. Labels stay aligned either way, but a mix reads as noise.
Don't:
- Submenus inside submenus. Two levels deep loses keyboard and pointer users alike. Flatten with groups.
Accessibility:
- Base UI Menu: the trigger has aria-haspopup and aria-expanded; the popup is role menu with menuitem, menuitemcheckbox and menuitemradio children.
- ArrowDown or Enter opens it and focuses the first item; arrows move, Home and End jump, typing a letter jumps to a matching item.
- Enter and Space activate; checkable items toggle aria-checked and keep the menu open.
- ArrowRight opens a submenu, ArrowLeft and Escape close it; Escape on the root returns focus to the trigger.
- Shortcut hints are visual only (aria-hidden); bind the shortcut yourself and document it elsewhere.
- Disabled items stay in the list so users learn the action exists, but cannot be activated.
Tokens used: --border-width-default, --focus-ring-color, --focus-ring-width, --font-family-mono, --font-family-sans, --font-line-height-snug, --font-size-100, --font-size-200, --font-tracking-wide, --font-weight-medium, --icon-size-sm, --layer-popover, --menu-item-bg-highlight, --menu-item-height, --menu-item-indicator, --menu-item-text, --menu-item-text-danger, --menu-item-text-disabled, --menu-item-text-muted, --menu-separator, --motion-duration-fast, --motion-easing-enter, --motion-easing-exit, --popup-bg, --popup-border, --popup-radius, --popup-shadow, --radius-pill, --size-60, --size-80, --space-1, --space-1-5, --space-2, --space-4
Docs facts: [/docs-ai/menu.json](/docs-ai/menu.json)
### Popover
A small non-modal panel anchored to a trigger, for extra detail or a quick choice that does not deserve a dialog: who must review, what a check does, branch info.
Import: `import { Popover } from '@bridge-ds/react'`
Props:
- open?: boolean
- defaultOpen?: boolean
- onOpenChange?: (open: boolean) => void
- modal?: boolean = false. Trap focus and block the page while open. Default false: popovers are light, non-modal surfaces.
- children?: ReactNode. PopoverTrigger and PopoverContent.
When to use:
- Explaining something next to the control it is about, when the text is too long for a tooltip or contains a link.
- A small form or a couple of actions tied to one control.
- Info icons: Popover (click or openOnHover) instead of Tooltip, so touch and screen reader users can reach it.
When not to use:
- A list of actions: use Menu, which has the right keyboard model.
- A label for an icon button: use Tooltip.
- A task that must be completed or cancelled: use Dialog.
Do:
- Keep it short and anchored. A title, two sentences, at most one or two actions. The arrow shows what it belongs to.
Don't:
- Forms that span several sections. A popover closes on an outside click, so long input gets lost. Use a Dialog or a Drawer.
Accessibility:
- Base UI Popover: the trigger exposes aria-expanded and aria-controls; the popup is a dialog named by its title.
- Focus moves into the popup when it opens and back to the trigger when it closes. Escape and outside clicks close it.
- Non-modal by default: the page stays usable. Set modal when the popover holds a small form.
- It flips and shifts to stay in the viewport; the arrow follows the anchor.
Tokens used: --border-width-default, --color-text-default, --color-text-muted, --font-family-sans, --font-line-height-normal, --font-line-height-snug, --font-size-200, --font-weight-semibold, --layer-popover, --motion-duration-fast, --motion-easing-enter, --motion-easing-exit, --popup-bg, --popup-border, --popup-radius, --popup-shadow, --size-80, --space-1-5, --space-2, --space-3, --space-4
Docs facts: [/docs-ai/popover.json](/docs-ai/popover.json)
### Tooltip
A short label that appears on hover and keyboard focus. It names icon buttons and reveals truncated text for sighted users; the accessible name always lives on the trigger.
Import: `import { Tooltip, TooltipProvider } from '@bridge-ds/react'`
Props:
- content: ReactNode. The tooltip text. Short, plain, no interactive content.
- children: ReactElement. The trigger: a focusable element that forwards its ref and spreads props (Button, IconButton, Link).
- side?: 'top' | 'bottom' | 'left' | 'right' = 'top'. Preferred side. Default `top`.
- align?: 'start' | 'center' | 'end' = 'center'
- delay?: number. Hover delay in ms for this tooltip; otherwise the TooltipProvider delay applies.
- open?: boolean
- defaultOpen?: boolean
- onOpenChange?: (open: boolean) => void
- disabled?: boolean = false. Turn the tooltip off without unwrapping the trigger.
- className?: string
When to use:
- Every IconButton, with the same text as its label.
- Truncated text, to show the full value.
- Wrap the app once in TooltipProvider so neighbouring tooltips open instantly after the first one.
- Plain-script pages without React put tooltipAttributes() from @bridge-ds/react/dom on the trigger and call bindTooltips() once: the same tooltip, delay and keyboard behavior. Never a homemade data-tip tooltip: the design system audit counts those as look-alikes.
When not to use:
- Information users need to complete a task: show it inline or in a Popover. Tooltips are invisible on touch screens.
- Interactive content (links, buttons): tooltips cannot be reached. Use Popover.
- Repeating a visible label: it adds noise.
Do:
- Match the trigger's label. IconButton label="Copy token name" and the tooltip "Copy token name": what users see and hear is the same.
Don't:
- Tooltips on disabled buttons to explain why. Disabled buttons do not receive focus, so keyboard users never see it. Explain inline instead.
Accessibility:
- Base UI treats tooltips as visual labels: the trigger must carry the accessible name (IconButton label), which screen readers announce. The popup is not announced.
- Opens on hover after the provider delay (500ms) and immediately on keyboard focus; Escape dismisses it.
- The trigger must be focusable: wrap a Button, IconButton or Link, never plain text.
- Inverse surface (tooltip.bg, tooltip.text) passes 4.5:1 in both themes.
Tokens used: --elevation-overlay, --font-family-sans, --font-line-height-snug, --font-size-100, --font-tracking-wide, --font-weight-medium, --layer-tooltip, --motion-duration-fast, --motion-duration-instant, --motion-easing-enter, --motion-easing-exit, --size-72, --space-1, --space-2, --tooltip-bg, --tooltip-radius, --tooltip-text
Docs facts: [/docs-ai/tooltip.json](/docs-ai/tooltip.json)
### Breadcrumb
Shows where the current page sits in the library hierarchy and links back to each level above it.
Import: `import { Breadcrumb } from '@bridge-ds/react'`
Props:
- items: BreadcrumbItemData[]. Path from the top of the hierarchy to the current page. The last item is marked as the current page.
- maxItems?: number. Accessible name of the navigation landmark. Default "Breadcrumb". */ 'aria-label'?: string; /** Collapse long paths: keep the first item and the last `maxItems - 1`, hide the middle behind a "Show path" button. Default: no collapsing.
When to use:
- Pages two or more levels deep in a hierarchy: a token inside a group, a component inside a family.
- At the top of the page, above the page title.
- maxItems on deep paths so the trail stays on one line; the middle levels stay one click away.
When not to use:
- Flat sites or the top level: there is nothing to go back up to.
- Showing the steps of a flow: use a stepper. A breadcrumb shows location, not progress.
- As the only navigation: it complements SidebarNav, it does not replace it.
Do:
- Mirror the hierarchy, not the history. Each item is a parent of the next, whatever route the user took to get here.
Don't:
- Linking the current page. The last item is where the user already is. It is text with aria-current, not a link.
Accessibility:
- A nav landmark named "Breadcrumb" (override with aria-label), with an ordered list inside.
- The last item has aria-current="page".
- Separators are icons hidden from assistive tech, so screen readers only read the levels.
- The collapse button is named "Show N more levels" and expands the trail in place.
- Truncated labels keep the full text in the title attribute and in the accessible name.
Tokens used: --border-width-1, --color-surface-hover, --color-text-default, --color-text-muted, --focus-ring-color, --focus-ring-width, --font-family-sans, --font-line-height-snug, --font-size-200, --font-tracking-wide, --font-weight-medium, --motion-duration-instant, --motion-easing-standard, --radius-inset, --size-6, --size-60, --space-1
Docs facts: [/docs-ai/breadcrumb.json](/docs-ai/breadcrumb.json)
### Pagination
Moves through a long list split into pages, like the token table or the review history. The first and last pages stay visible; the rest collapse into ellipses.
Import: `import { Pagination, getPageItems } from '@bridge-ds/react'`
Props:
- pageCount: number. Total number of pages.
- page?: number. Current page, 1-based (controlled).
- defaultPage?: number = 1. First page shown when uncontrolled. Default 1.
- onPageChange?: (page: number) => void. Called with the requested page, 1-based.
- siblingCount?: number = 1. Pages shown on each side of the current one before an ellipsis. Default 1.
- variant?: 'full' | 'compact' = 'full'. `full` lists page numbers; `compact` shows "Page 3 of 12" between previous and next (narrow spaces). Default `full`.
- size?: 'sm' | 'md' = 'md'. Button height from the density tokens. Default `md`.
When to use:
- Long, stable lists where users need to know where they are and jump back: token tables, audit logs.
- Below the list it controls, aligned with the list's start or end edge.
- compact in narrow spaces (side panels, mobile) and in card footers.
When not to use:
- Feeds the user skims once: load more or infinite scroll fit better.
- Fewer than about 25 items: show them all.
- Steps of a task: use a stepper.
Do:
- Keep the page in the URL. Reloading or sharing the link should land on the same page. Control page and onPageChange from your router.
Don't:
- Hiding previous and next at the ends. Disabled previous on page 1 keeps the layout still and tells the user they are at the start.
Accessibility:
- A nav landmark named "Pagination" (override with aria-label when a page has several).
- The current page button has aria-current="page" and an ink fill, not color alone.
- Page buttons are named "Page N"; previous and next are named "Previous page" and "Next page" and are disabled at the ends.
- Ellipses are icons hidden from assistive tech.
- In compact, the "Page 3 of 12" text is a polite live region, so page changes are announced.
Tokens used: --border-width-default, --button-ghost-bg-active, --button-ghost-bg-hover, --color-control-checked-bg, --color-control-checked-fg, --color-text-default, --color-text-muted, --control-height-md, --control-height-sm, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-sans, --font-size-200, --font-weight-medium, --motion-duration-instant, --motion-easing-standard, --radius-control, --space-1, --space-2
Docs facts: [/docs-ai/pagination.json](/docs-ai/pagination.json)
### SidebarNav
The app's side navigation: sections of links to the main areas of Bridge, with the current page marked by a signal mark and an ink label.
Import: `import { SidebarNav } from '@bridge-ds/react'`
Props:
- children?: ReactNode
When to use:
- Primary navigation of an app with more than five areas, in a left rail that stays visible.
- Group items into titled sections by task (Library, Governance, Workspace).
- collapsible on sections users rarely need, so the rail stays short.
- count for items that need attention, like reviews waiting for you.
When not to use:
- Switching views inside one page: use Tabs.
- Fewer than five destinations: a top bar is enough.
- Actions (Create token, Sync from Figma): put them in the page header as Buttons, not in the navigation.
Do:
- Mark exactly one current page. active on the item for the current URL, including when the user is on a child page of it.
Don't:
- Icons on some items only. A ragged column of icons breaks the scan line. Use icons on every item of a section or none.
Accessibility:
- A nav landmark: give it an aria-label when the page has other navigation (Breadcrumb, Pagination).
- Each section is a list labelled by its title.
- The current item has aria-current="page". It also gets a 2px signal mark and ink text, so color is not the only cue.
- Collapsible section titles are buttons with aria-expanded (Base UI Collapsible); Enter and Space toggle them.
- Items are full-width targets at the menu item height (36px comfortable, 30px compact).
Tokens used: --border-width-indicator, --color-bg-muted, --color-text-default, --color-text-muted, --focus-ring-color, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-line-height-snug, --font-size-100, --font-size-200, --font-tracking-wide, --font-weight-medium, --menu-item-height, --motion-duration-fast, --motion-duration-instant, --motion-duration-moderate, --motion-easing-enter, --motion-easing-standard, --nav-indicator, --nav-item-bg-active, --nav-item-bg-hover, --radius-control, --radius-pill, --size-7, --space-0-5, --space-1, --space-1-5, --space-2, --space-3, --space-5
Docs facts: [/docs-ai/sidebar-nav.json](/docs-ai/sidebar-nav.json)
### Tabs
Switches between views of the same object without leaving the page: the tokens, components and reviews of one library. The selected tab is marked with the signal underline.
Import: `import { Tabs, watchOverflowEdges } from '@bridge-ds/react'`
Accepts all native <div> attributes except defaultValue, onChange.
Props:
- variant?: 'underline' | 'contained' = 'underline'. `underline` for page-level sections (signal underline); `contained` for a compact segmented switch. Default `underline`.
- size?: 'sm' | 'md' = 'md'. Tab height follows density tokens. Default `md`.
- defaultValue?: string | number | null. Value of the tab selected on first render (uncontrolled).
- value?: string | number | null. Value of the selected tab (controlled).
- onValueChange?: (value: TabsValue) => void. Called with the new value when the user selects a tab.
- orientation?: 'horizontal' | 'vertical' = 'horizontal'
- children?: ReactNode
When to use:
- Two to seven peer views of the same context, where users switch back and forth.
- underline for page and card sections; contained for a compact switch inside a toolbar (Light or Dark preview).
- count to show how many items wait in a view, like open reviews.
- Vertical orientation for settings pages with many sections next to their content.
When not to use:
- Steps of a sequence the user must complete in order: use a stepper or a form with sections.
- Navigation between pages with their own URL: use SidebarNav or links styled as tabs, not Tabs (which do not change the URL).
- Changing a setting or filter value: use RadioGroup or an attached ButtonGroup.
Do:
- Short, parallel labels. One or two nouns per tab, same grammatical form. The user scans them as a set.
Don't:
- Tabs that are really actions. "Export" or "Delete" in a tab list hides an action behind a view switch. Use a Button.
Accessibility:
- Base UI Tabs: role tablist, tab and tabpanel, aria-selected, aria-controls and aria-labelledby are wired for you.
- One tab stop for the whole list. Arrow keys move between tabs (Up and Down when vertical), Home and End jump, focus wraps by default.
- By default arrows move focus and Enter or Space selects; set activateOnFocus when panels are cheap to render.
- Give the TabsList an aria-label (or labelledby) so the tablist has a name.
- The selected tab has a visible signal underline plus stronger text: never color alone.
- Disabled tabs are announced as disabled and cannot be selected.
Tokens used: --border-width-default, --border-width-indicator, --color-bg-subtle, --color-border-default, --color-surface-default, --color-text-default, --color-text-disabled, --control-height-md, --control-height-sm, --elevation-raised, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-line-height-tight, --font-size-100, --font-size-200, --font-weight-medium, --layout-gap, --motion-duration-instant, --motion-duration-moderate, --motion-easing-standard, --radius-container, --radius-control, --radius-pill, --space-0-5, --space-1, --space-1-5, --space-10, --space-2, --space-3, --space-5, --tabs-indicator, --tabs-text, --tabs-text-active
Docs facts: [/docs-ai/tabs.json](/docs-ai/tabs.json)
### Accordion
Stacked sections that expand in place. Lets users scan headings and open only what they need.
Import: `import { Accordion } from '@bridge-ds/react'`
Props:
- children: ReactNode
- multiple?: boolean = false. Allow several panels open at once. Default `false`.
- defaultValue?: string[]. Values of the items open by default (uncontrolled).
- value?: string[]. Values of the open items (controlled).
- onValueChange?: (value: string[]) => void
- headingLevel?: 2 | 3 | 4 | 5 | 6 = 3. Heading level of every item trigger, to fit the page outline. Default `3`.
- appearance?: 'contained' | 'flush' = 'contained'. `contained` draws an outer border (inside a page); `flush` only separates items (inside a card). Default `contained`.
- disabled?: boolean
- className?: string
When to use:
- Long reference content where users look for one section: token groups, FAQ, advanced settings.
- multiple when users compare sections; single (default) when sections are alternatives.
- flush inside a card or a side panel, contained on a page.
When not to use:
- Content everyone needs: do not hide it behind a click.
- Switching between views of the same data: use Tabs.
- Steps in a sequence: use a stepped layout with headings.
Do:
- Headings that work as a table of contents. Users scan the closed state. Name each section by its content, and show a count in meta when it helps.
Don't:
- Nesting accordions. A collapsible inside a collapsible hides content twice. Flatten the structure or split the page.
Accessibility:
- Base UI Accordion: each trigger is a button inside a heading (headingLevel, default h3) with aria-expanded and aria-controls.
- Panels are regions labelled by their trigger. Enter and Space toggle; Tab moves between triggers.
- Disabled items stay visible but cannot open.
- The height animation uses motion tokens and collapses to 0ms with reduced motion.
Tokens used: --accordion-bg-hover, --accordion-border, --accordion-icon, --accordion-text, --border-width-default, --color-border-strong, --color-text-default, --color-text-disabled, --color-text-muted, --control-height-lg, --focus-ring-color, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-line-height-snug, --font-size-100, --font-size-200, --font-weight-medium, --font-weight-regular, --layout-inset, --motion-duration-instant, --motion-duration-moderate, --motion-easing-standard, --radius-container, --space-2, --space-3
Docs facts: [/docs-ai/accordion.json](/docs-ai/accordion.json)
### Avatar
A person, a team or Claude, shown as a photo with an initials or illustrated fallback. AvatarGroup stacks several and collapses the rest into a +N counter.
Import: `import { Avatar, initials, avatarName } from '@bridge-ds/react'`
Accepts all native <span> attributes.
Props:
- name?: string. Full name. Used for the initials fallback and as the accessible name. Optional for an agent preset.
- src?: string. Image URL. While it loads or if it fails, the fallback shows instead.
- size?: 'xs' | 'sm' | 'md' | 'lg' = 'md'. xs 20, sm 24, md 32, lg 40. Default `md`.
- decorative?: boolean = false. Hide from assistive tech when the name is already written next to the avatar.
- fallback?: 'initials' | 'illustrated' = 'initials'. `initials` (default) or `illustrated`: a drawn face that is always the same for the same seed. The drawing comes from `illustration`; the Avatar exported by `@bridge-ds/react/avatar-illustrated` fills it from the seed for you.
- seed?: string. What the illustrated face is drawn from, for example a login. Default: the name.
- illustration?: string. Image (URL or data URI) of the illustrated face. Without it, `fallback="illustrated"` shows initials.
- kind?: 'person' | 'agent'. `person` (default), or `agent`: an AI acting in the product, so people can tell its work from a person's.
- brand?: 'claude'. With `kind="agent"`: `claude` names the agent "Claude AI" and shows the Bridge agent mark, the Field Guide robot, on its tile.
- mark?: ReactNode. With `kind="agent"`: replaces the Bridge agent mark (the Field Guide robot), for example with a vendor logo (an SVG that fills its box). Only for a logo you have the brand owner's approval to use. Decorative: the name stays the accessible name.
When to use:
- Owners, reviewers and authors next to tokens, components and changes.
- fallback="illustrated" when most people have no photo: a drawn face from their login tells rows apart faster than two letters. Import Avatar from @bridge-ds/react/avatar-illustrated (the drawings are a separate entry, about 120 kB gzipped).
- kind="agent" brand="claude" for work Claude did (a drafted text, a change it opened): the Bridge agent mark (the Field Guide robot: a rounded head with an antenna, dot eyes and a smile) on a rounded square tile, named "Claude AI", so people see at a glance it is not a person.
- mark only to show a vendor logo you have the brand owner's approval to use: Bridge ships its own agent mark because company logos are trademarks that need their owner's approval.
- AvatarGroup for the reviewers of a change; set max so the row never wraps.
When not to use:
- As the only way to identify someone in a table: write the name too.
- For product or brand logos: use an image with its own alt text. The agent preset only attributes an agent's actions.
- To make up a face for someone who has a photo: the illustration is a fallback, the photo wins.
Do:
- Name next to the avatar in lists. Faces are hard to tell apart at 24px. Mark the avatar decorative when the name is right beside it.
- Seed with something stable. Seed the illustrated face with the login, not the display name: people rename themselves, their face should stay.
Don't:
- Ten avatars in a row. Past four or five faces nobody counts. Let the +N counter carry the rest.
- Claude dressed as a person. Do not give Claude a drawn face or initials. Use the agent preset so nobody mistakes its work for a teammate's.
Accessibility:
- role img with the full name as its label; initials and drawings are hidden from screen readers so they do not spell letters or describe a face.
- The Claude preset is named "Claude AI"; its mark (the Bridge robot or a custom mark) is decorative inside it.
- The robot is one stroke in currentColor with no fill, so it keeps the tile's checked contrast in light and dark, and still reads at the xs size (16px mark).
- decorative removes it from the accessibility tree when the name is already written next to it.
- The image has an empty alt: the name is on the container. Broken images fall back to initials or the drawing (Base UI Avatar).
- AvatarGroup is a named group; the +N counter is announced as "3 more: names".
Tokens used: --avatar-agent-bg, --avatar-agent-border, --avatar-bg, --avatar-illustrated-bg, --avatar-ring, --avatar-text, --border-width-2, --border-width-default, --color-action-primary-bg, --color-bg-subtle, --color-text-muted, --font-family-sans, --font-line-height-none, --font-size-100, --font-size-200, --font-weight-semibold, --radius-inset, --radius-pill, --size-10, --size-4, --size-5, --size-6, --size-7, --size-8
Docs facts: [/docs-ai/avatar.json](/docs-ai/avatar.json)
### Badge
A short, non-interactive status label like Draft, Published or Failed. The words carry the meaning; the tone reinforces it.
Import: `import { Badge } from '@bridge-ds/react'`
Accepts all native <span> attributes.
Props:
- tone?: 'neutral' | 'success' | 'warning' | 'danger' | 'info' = 'neutral'. Meaning of the status. The text must say it too: tone only reinforces.
- appearance?: 'subtle' | 'solid' = 'subtle'. `subtle` (tinted, default) for dense lists and tables; `solid` for the one status that must win a glance.
- size?: 'sm' | 'md' = 'md'
- dot?: boolean = false. Leading dot in the tone color. Ignored when an icon is set.
- icon?: LucideIcon. Leading Lucide icon, for example CircleCheck for success.
When to use:
- The status of one item in a table row, a list or a card header.
- subtle (default) in lists and tables where many badges sit together; solid for the single status that must win a glance (Breaking, Build failed).
- Tone by meaning: success done or healthy, warning needs attention, danger failed or blocked, info new or changed, neutral no judgement.
- Plain-script pages that build rows as HTML strings use badgeHtml() from @bridge-ds/react/dom: the same markup, tones and sizes, without React. Never a homemade pill: the design system audit counts those as look-alikes.
When not to use:
- Anything clickable or removable: use Tag.
- A sentence or an explanation: use an inline Alert.
- Counts on icons: use the number in text next to the label.
Do:
- The text says the status, the tone backs it up. Someone who cannot tell green from red still reads Published and Failed.
- Subtle in rows, solid for the exception. A table full of solid badges shouts. Keep solid for the one thing that must stand out.
Don't:
- Color as the only signal. A dot without words fails WCAG 1.4.1 and means nothing to a screen reader.
- A tone that contradicts the words. Danger for New borrows urgency the user did not ask for.
Accessibility:
- Static text read in reading order. No role, not focusable.
- Subtle tones use feedback text on feedback bg (4.5:1); solid tones use text.on-solid on feedback solid (4.5:1). Checked by the token build in every brand and theme.
- The dot and icon are decorative; the text carries the status.
- If a status changes while the user watches (Running to Failed), announce it with a live region; the badge itself is silent.
Tokens used: --border-width-default, --color-border-default, --color-feedback-danger-bg, --color-feedback-danger-border, --color-feedback-danger-icon, --color-feedback-danger-solid, --color-feedback-danger-text, --color-feedback-info-bg, --color-feedback-info-border, --color-feedback-info-icon, --color-feedback-info-solid, --color-feedback-info-text, --color-feedback-neutral-bg, --color-feedback-success-bg, --color-feedback-success-border, --color-feedback-success-icon, --color-feedback-success-solid, --color-feedback-success-text, --color-feedback-warning-bg, --color-feedback-warning-border, --color-feedback-warning-icon, --color-feedback-warning-solid, --color-feedback-warning-text, --color-surface-inverse, --color-text-default, --color-text-inverse, --color-text-muted, --color-text-on-solid, --font-family-sans, --font-tracking-wide, --font-weight-medium, --radius-pill, --size-5, --size-6, --space-0-5, --space-1, --space-1-5, --space-2, --typography-caption-line-height, --typography-caption-size, --typography-ui-line-height, --typography-ui-size
Docs facts: [/docs-ai/badge.json](/docs-ai/badge.json)
### Card
A flat plate with a hairline edge that groups related content: a release, a component summary, a settings section. It does not cast a shadow unless the whole card is one link.
Import: `import { Card } from '@bridge-ds/react'`
Accepts all native <div> attributes.
Props:
- interactive?: boolean = false. The whole card is one link or action: it lifts on hover, and the element marked `data-card-primary` (a link or button whose text names the destination, usually the title) covers the card and moves its focus ring to the card. Other buttons inside stay clickable on top.
- padding?: 'none' | 'compact' | 'default' = 'default'. Inner spacing of header, body and footer. `none` for full-bleed tables and media. Default `default`.
When to use:
- A self-contained unit the user scans or compares: a release, a component, a token group, an owner.
- interactive when the whole card goes to one place (a component page): mark its title link with data-card-primary.
- padding none for a full-bleed Table or media inside the card; compact in dense dashboards.
When not to use:
- As the page structure: rows of identical cards are a lazy layout. Use headings, lists and tables first.
- Inside another card: nested plates blur hierarchy. Use a Divider or a heading.
- Floating content (menus, popovers): those are overlays with elevation, not cards.
Do:
- One primary action in the footer. The footer is for the decision; secondary actions step down to ghost.
Don't:
- A card around a single sentence. A box with one line adds noise. Let the text sit on the page.
Accessibility:
- A card is a plain div. Pass aria-labelledby pointing at its title when it should be a navigable group.
- Interactive cards keep a real link (data-card-primary) whose text names the destination; its click area is stretched over the card and its focus ring moves to the card edge.
- Other buttons inside an interactive card stay on top and remain separately focusable.
- The hover lift is skipped when the user prefers reduced motion.
Tokens used: --border-width-1, --border-width-default, --card-bg, --card-border, --card-radius, --card-shadow, --color-bg-subtle, --color-border-hover, --color-text-default, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-size-200, --layout-inset, --motion-duration-fast, --motion-easing-standard, --space-2, --space-3, --space-4, --typography-h3-family, --typography-h3-line-height, --typography-h3-size, --typography-h3-tracking, --typography-h3-weight
Docs facts: [/docs-ai/card.json](/docs-ai/card.json)
### Checklist
The steps or conditions something needs, one row each: a status icon, a title, one line of detail and, when there is one, the action on the same row. Like the merge box of a pull request.
Import: `import { Checklist } from '@bridge-ds/react'`
Accepts all native <HTMLUListElement> attributes.
Props:
- label?: string. Accessible name of the list: what the rows add up to, "What it needs to ship".
- footer?: ReactNode. Text and actions under the rows, in the same plate: why something waits, a way to unblock it.
- children?: ReactNode. ChecklistRow elements.
When to use:
- What a change needs before it ships: design rules, developer tests, design approval.
- A short setup the reader goes through once, where each step has a clear done state.
- Put the action for a row on that row (an Approve button next to Design approval), as a Button with size sm and variant secondary. On narrow screens it takes the full width under the row.
- Use footer for what the rows add up to: why it waits, and the one way to unblock it.
- Plain-script pages that build rows as HTML strings use checklistHtml() and checklistRowHtml() from @bridge-ds/react/dom: the same markup, status icons and screen reader words, without React.
When not to use:
- Something the reader ticks off: use Checkbox. A Checklist row shows a status the system knows, it is not a control.
- A single message about the page: use Alert.
- A long log of events in time: use a list or a table with dates.
- Steps the reader moves through one screen at a time: use a stepper or Tabs.
Do:
- Say who or what each row waits on. The detail turns a status into a next step: "Waiting on a design owner" tells the reader whom to ask.
- Name what failed, with numbers. "2 of 212 tests fail" says how bad it is and where to look. "Tests failed" does not.
Don't:
- A title without a detail. An icon and a title alone leave the reader guessing what to do. Every row that is not done says why in one line.
- Warning for something that simply waits. Waiting is normal: use the empty ring. Keep warning for a row that needs attention, such as an approval given before the last change.
Accessibility:
- A list (ul) named by label: "What it needs to ship, list, 3 items".
- Each row says its status in words before the detail (Done, Failed, Waiting, Needs attention), in visually hidden text. The icon is decorative, so color and shape are never the only signal. Override the words per row with statusLabel.
- A row's action is a real Button on the row, reached with Tab in reading order, after the row it acts on.
- Feedback icon colors are checked against their tinted backgrounds in both themes.
- Static markup: no hydration needed. Plain scripts render the same markup with checklistHtml() and checklistRowHtml() from @bridge-ds/react/dom.
Tokens used: --border-width-default, --border-width-strong, --color-border-default, --color-border-emphasis, --color-feedback-danger-bg, --color-feedback-danger-text, --color-feedback-success-bg, --color-feedback-success-text, --color-feedback-warning-bg, --color-feedback-warning-text, --color-surface-default, --color-text-default, --color-text-muted, --control-height-md, --font-family-sans, --font-weight-semibold, --radius-container, --radius-pill, --size-5, --size-6, --space-0-5, --space-1, --space-2, --space-3, --space-4, --typography-ui-line-height, --typography-ui-size
Docs facts: [/docs-ai/checklist.json](/docs-ai/checklist.json)
### CodeBlock
A command or a short piece of code in IBM Plex Mono, with a copy button on the same line. The button says Copied for two seconds, so the reader knows the paste will work.
Import: `import { CodeBlock } from '@bridge-ds/react'`
Accepts all native <div> attributes except children, onCopy.
Props:
- code: string. The code or command, shown as is and copied as is.
- label?: ReactNode. What the code is, above the block: "Install", "Add the MCP server". Also names the copy button.
- wrap?: boolean. Commands wrap, code scrolls. By default a one-line command wraps at spaces only (never inside a package name or a flag), so it is read whole and never hides behind the copy button; code with several lines keeps its layout and scrolls sideways inside the block. Pass `true` or `false` to choose.
- copyLabel?: string = 'Copy'. The copy button text. Default "Copy".
- copiedLabel?: string = 'Copied'. The button text after a copy, for two seconds. Default "Copied".
- hideCopy?: boolean = false. Hide the copy button (code the reader should read, not paste).
- onCopy?: (code: string) => void. Called after the code reached the clipboard.
- codeProps?: HTMLAttributes<HTMLElement> & { [key: `data-${string}`]: string | undefined }. Attributes on the `<code>` element, for example an `id` a script reads or fills.
When to use:
- A command the reader runs or pastes: an install line, an MCP server setup, a CLI call.
- A short snippet the reader copies as is: a CSS variable in use, an import line.
- Commands wrap, code scrolls (the default): a one-line command breaks at spaces only, never inside a package name or a flag, so the reader sees all of it; code with several lines keeps its layout and scrolls sideways inside the block. Set wrap to choose.
- hideCopy for code the reader should read, not paste: a file path, an example output.
- Plain-script pages that build rows as HTML strings use codeBlockHtml() and bindCodeBlocks() from @bridge-ds/react/dom: the same markup and the same copy behavior, without React.
When not to use:
- A token name or value inside a sentence or a table cell: use TokenValue.
- A whole file or a long diff: link to it, or show it in a dedicated viewer.
- Inline code in running text: use the code element of the page's prose styles.
Do:
- Name what the code does. A short label above the block (Install, Add the MCP server) tells the reader which block to copy, and names the copy button for screen readers: Copy install.
- One command per block. Two steps are two blocks, each with its own label and copy button. The reader pastes one thing at a time and sees which one worked.
Don't:
- A prompt sign in the code. The block copies exactly what it shows. A leading $ ends up in the terminal and the command fails.
- Several commands chained in one block. A row of commands joined with && is hard to read and fails as a whole. Split it into one block per step.
Accessibility:
- The copy button is a real Button. With a string label it is named Copy plus the label (Copy install); after a copy it reads Copied.
- A polite live region announces Copied to the clipboard. If the clipboard is blocked, the code is selected and the region says to press Ctrl+C or Cmd+C.
- A block that does not wrap is keyboard focusable (tabindex 0), so a long line can be scrolled with the arrow keys. A wrapped block needs no focus stop.
- Copy works without hydration: statically rendered blocks carry data-code-block hooks and bindCodeBlocks() from @bridge-ds/react/dom wires them. Plain scripts render the same markup with codeBlockHtml().
- Code text uses color.text.default on color.bg.subtle, checked in both themes.
Tokens used: --border-width-default, --color-bg-subtle, --color-border-default, --color-text-default, --color-text-muted, --control-height-sm, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-mono, --font-family-sans, --font-tracking-wide, --font-weight-medium, --radius-control, --radius-inset, --space-1, --space-1-5, --space-2, --space-3, --typography-caption-line-height, --typography-caption-size, --typography-ui-line-height, --typography-ui-size
Docs facts: [/docs-ai/code-block.json](/docs-ai/code-block.json)
### Divider
A hairline that separates groups of content, horizontally or vertically, with an optional short label.
Import: `import { Divider } from '@bridge-ds/react'`
Accepts all native <div> attributes.
Props:
- orientation?: 'horizontal' | 'vertical' = 'horizontal'
- label?: ReactNode. Short text centered on a horizontal line, like "Or" or "Older changes".
- spacing?: 'none' | 'sm' | 'md' = 'md'. Space around the line: `none`, `sm` (8px) or `md` (16px). Default `md`.
When to use:
- Between groups that belong to the same surface but differ in kind: settings sections, toolbar groups.
- Labelled, to name what follows ("Older changes") or to offer an alternative ("Or").
When not to use:
- Between every list item: use spacing, or row borders in a Table.
- To separate sections that have headings: the heading and space already do it.
Do:
- Vertical dividers between toolbar groups. They group related controls without adding boxes.
Don't:
- Stacked dividers and borders. A divider next to a card edge doubles the line. Pick one.
Accessibility:
- Base UI Separator: role separator with aria-orientation, announced as a boundary.
- The labelled variant renders two separators around plain text, so the label is read once in order.
- The line uses border.default (decorative); never rely on it to convey meaning.
Tokens used: --border-width-default, --divider-line, --divider-text, --font-family-sans, --font-size-100, --font-tracking-wide, --font-weight-medium, --size-4, --space-2, --space-3, --space-4
Docs facts: [/docs-ai/divider.json](/docs-ai/divider.json)
### Kbd
Shows a keyboard key or a key combination, as nested kbd elements in the mono face.
Import: `import { Kbd } from '@bridge-ds/react'`
Props:
- keys: string | string[]. One key ("Esc") or a combination (["Cmd", "K"]) pressed together.
- size?: 'sm' | 'md' = 'md'
When to use:
- Teaching a shortcut in a tooltip, a menu item, an empty state or help text.
- Inside an Input suffix to hint at the search shortcut.
When not to use:
- Code or token names: use code.
- Buttons: a Kbd is not clickable.
Do:
- Write the platform's key names. Cmd on macOS, Ctrl elsewhere. Abbreviations get a title with the full name.
Don't:
- Long sequences. More than three keys is a recipe, not a shortcut. Explain it in text.
Accessibility:
- Outer kbd wraps one kbd per key, the HTML pattern for keys pressed together; the plus signs are aria-hidden.
- Abbreviated keys (Cmd, Esc, arrows) carry a title with the full name.
- Text color is text.muted on surface (4.5:1).
Tokens used: --border-width-default, --color-text-muted, --font-family-mono, --font-family-sans, --font-line-height-none, --font-size-100, --font-weight-medium, --kbd-bg, --kbd-border, --kbd-border-bottom, --kbd-radius, --kbd-text, --size-4, --size-5, --space-0-5, --space-1
Docs facts: [/docs-ai/kbd.json](/docs-ai/kbd.json)
### Table
A semantic data table for comparing records: sortable columns, selectable rows, density-aware rows, and a focusable scroll region on narrow screens.
Import: `import { Table, useTableSort } from '@bridge-ds/react'`
Accepts all native <HTMLTableElement> attributes.
Props:
- caption?: ReactNode. Visible caption above the table. Also names the scroll region.
- stickyHeader?: boolean = false. Accessible name when there is no visible caption, for example "Tokens changed in this pull request". */ 'aria-label'?: string; /** Keep the header row visible while the table scrolls inside its container.
- maxHeight?: string. Max height of the scroll container (CSS length from a token, for example var(--size-120)). Enables vertical scroll.
- gridLines?: boolean = false. Hairlines between columns as well as rows. Default `false`.
- layout?: 'auto' | 'fixed' = 'auto'. `auto` (default) sizes columns from their content and the widths you set. `fixed` follows the widths of the header cells exactly and truncates nothing on its own: long words wrap. Use `fixed` when rows should not shift as data loads.
- narrowLayout?: 'scroll' | 'stack' = 'scroll'. In a container narrower than about 480px: `scroll` keeps the columns and scrolls sideways (default); `stack` shows each row as a card, every value beside its column name. Column names come from the header row, or from `label` on a cell.
When to use:
- Records with the same attributes that users compare, sort or act on: tokens, components, pull requests.
- Sortable headers (sortDirection + onSort, or useTableSort) on columns users reorder by.
- Selectable rows: a Checkbox in the first cell, selected on the row, bulk actions above the table.
- Compact density for expert, data-heavy screens.
- Column widths when one column should not take the space of the others: width on the header cell, as a token length (var(--size-20)), a percentage or a fraction (0.35), plus minWidth for a floor. layout fixed keeps them exact while data loads.
- narrowLayout stack when people read the table on a phone: under about 480px each row becomes a card, every value beside its column name. Keep scroll (the default) for wide data people compare across rows.
When not to use:
- Layout: never use a table to position content.
- Fewer than three attributes per item: use a list.
- Editing many cells inline like a spreadsheet: that needs a grid pattern, not in the system yet.
Do:
- Right-align numbers. numeric aligns digits on the right with tabular figures, so values compare at a glance.
- Say what to do when it is empty. TableEmpty keeps the header in place and explains how rows will appear.
Don't:
- Color as the only row state. A tinted row alone does not say selected. Pair it with a checked Checkbox.
Accessibility:
- Native table, thead, th scope col and td: screen readers announce headers with each cell.
- Sortable headers contain a button; the th gets aria-sort ascending or descending on the sorted column only.
- Selected rows set aria-selected and use the selected surface plus a checked Checkbox (never color alone).
- The scroll container is a labelled region with tabIndex 0, so keyboard users can scroll wide tables (named by the caption or aria-label).
- Row height and cell padding follow density: 48/36px rows, both above the 24px target minimum for the controls inside.
- Stacked rows keep their table roles (table, row, cell), so screen readers still read cell by cell; the column names shown before each value are the header texts. The header row is hidden while rows stack, so put sorting somewhere else on phones.
Tokens used: --border-width-default, --card-border, --color-border-default, --color-signal-default, --color-surface-default, --color-text-default, --color-text-muted, --control-height-md, --focus-ring-color, --focus-ring-offset, --focus-ring-width, --font-family-sans, --font-line-height-normal, --font-line-height-snug, --font-size-100, --font-size-200, --font-size-300, --font-tracking-wide, --font-weight-medium, --font-weight-semibold, --motion-duration-instant, --motion-easing-standard, --radius-container, --radius-inset, --size-6, --space-1, --space-2, --space-3, --space-8, --table-border, --table-cell-padding-x, --table-header-bg, --table-header-text, --table-row-bg-hover, --table-row-bg-selected, --table-row-height
Docs facts: [/docs-ai/table.json](/docs-ai/table.json)
### Tag
A compact label for a value. Static to describe, toggleable as a filter chip, or removable to show selected values.
Import: `import { Tag } from '@bridge-ds/react'`
Accepts all native <span> attributes except children.
Props:
- children: ReactNode. The tag text: a filter value, a token group, a component name.
- tone?: 'neutral' | 'signal' = 'neutral'. `signal` marks a tag that matches the current search or filter. Default `neutral`.
- icon?: LucideIcon. Leading Lucide icon.
- selected?: boolean. Makes the tag a toggle (filter chip). Controlled pressed state, announced with aria-pressed.
- defaultSelected?: boolean. Uncontrolled initial selected state for a toggle tag.
- onSelectedChange?: (selected: boolean) => void. Called when a toggle tag is pressed. Passing it makes the tag interactive.
- onRemove?: () => void. Adds a remove button. Called when it is pressed.
- removeLabel?: string. Accessible name of the remove button, without the verb. Defaults to the tag text when it is a string.
- disabled?: boolean = false
- size?: 'sm' | 'md' = 'md'
When to use:
- Filter chips above a table or a list: toggle tags in a labelled group.
- Selected values in a multi-select Combobox or a filter bar: removable tags.
- Static metadata: token groups, platforms, component categories.
When not to use:
- Status (Published, Failed): use Badge.
- Actions: use Button. A tag describes a value; it does not do something.
- Choosing one option out of several: use RadioGroup or Tabs.
Do:
- Put filter chips in a named group. role group with aria-label ("Filter by theme") tells screen reader users what the chips control.
Don't:
- Sentences in tags. Tags truncate. Keep them to a value of one to three words.
Accessibility:
- Toggle tags are Base UI Toggle buttons with aria-pressed; selected also shows a check icon, so selection is not color alone.
- The remove button is a separate button named "Remove <label>" (removeLabel when the text is not a plain string).
- Both buttons have the focus ring and are reachable with Tab; Enter and Space activate them.
- Static tags are plain text with no role.
Tokens used: --border-width-default, --color-action-disabled-bg, --color-action-disabled-text, --color-surface-pressed, --color-text-default, --focus-ring-color, --focus-ring-width, --font-family-sans, --font-line-height-tight, --font-size-100, --font-tracking-wide, --font-weight-medium, --motion-duration-instant, --motion-easing-standard, --size-5, --size-6, --space-0-5, --space-1, --space-2, --tag-bg, --tag-bg-hover, --tag-bg-selected, --tag-border, --tag-radius, --tag-signal-bg, --tag-signal-text, --tag-text, --tag-text-selected
Docs facts: [/docs-ai/tag.json](/docs-ai/tag.json)
### TokenValue
A token name and its value in IBM Plex Mono, with a color swatch when the value is a color. For token tables, before and after values, and the values behind a conflict choice.
Import: `import { TokenValue, breakAfterDots } from '@bridge-ds/react'`
Accepts all native <span> attributes except children.
Props:
- value: string. The value as written: a literal (`#0B4A8B`, `16px`) or an alias (`{color.harbor.700}`).
- name?: string. The token name, `color.action.primary.bg`. Omit it where the row already names the token.
- resolved?: string. The resolved value when `value` is an alias, shown muted after it: `#0B4A8B`.
- swatch?: string | false. The color of the swatch. By default the swatch shows when `resolved` or `value` is a color literal (hex, rgb, hsl, oklch, oklab, lab, lch, color()). Pass a color to force it, `false` to hide it.
- previous?: boolean = false. The value before a change: muted and struck through, read as a deletion. Pair it with the new value.
- size?: 'sm' | 'md' = 'md'. Default `md` (ui text). `sm` uses caption text, for dense tables.
When to use:
- Showing a token in a table, a review or a change list: the name, the value as written, and the resolved value when it is an alias.
- Before and after a change: the previous value with previous (struck through), then the new value.
- Inside a ChoiceCard description, so the reader sees the value each choice keeps.
- size sm in dense tables and inside sentences; md everywhere else.
- Plain-script pages that build rows as HTML strings use tokenValueHtml() from @bridge-ds/react/dom: the same markup and swatch rules, without React.
When not to use:
- A command or a snippet the reader copies: use CodeBlock.
- A status such as Changed or Removed: use Badge next to the value.
- Editing a value: use an Input, and show the TokenValue as the current value next to it.
Do:
- Show the alias and what it resolves to. The alias says where the value comes from, the resolved value says what the reader will see. Together they answer both questions in one glance.
- Pair a previous value with the new one. The struck-through value reads as removed, the plain one as the new value. Keep them on one line, old first.
Don't:
- Repeating the token name in its own row. When the row or column already names the token, drop name and show the value only. The same name twice is noise.
- A previous value on its own. A struck-through value without the new one tells the reader what is gone but not what replaces it.
Accessibility:
- Static text, no interaction: screen readers read the name, the value and the resolved value in order.
- The swatch is decorative (aria-hidden): the value text always says the color, so the swatch is never the only signal.
- A previous value is wrapped in del, so assistive technology that reports deletions reads it as removed, and it is struck through for sighted readers.
- The swatch carries a hairline border, so white and near-background colors still read as a square in both themes.
- Long names and aliases break anywhere instead of overflowing a narrow cell.
- Plain scripts render the same markup with tokenValueHtml() from @bridge-ds/react/dom.
Tokens used: --border-width-default, --color-bg-subtle, --color-border-default, --color-border-emphasis, --color-surface-default, --color-text-default, --color-text-muted, --font-family-mono, --icon-size-sm, --icon-size-xs, --radius-inset, --radius-xs, --space-0-5, --space-1-5, --typography-caption-line-height, --typography-caption-size, --typography-ui-line-height, --typography-ui-size
Docs facts: [/docs-ai/token-value.json](/docs-ai/token-value.json)
### Container
Centers page content at a max width from the size scale with token side padding. Phones always get a 16px gutter.
Import: `import { Container } from '@bridge-ds/react'`
Accepts all native <div> attributes.
Props:
- size?: 'sm' | 'md' | 'lg' | 'xl' | 'full' = 'xl'. Max width: sm 640 (forms, reading), md 720, lg 960 (settings), xl 1200 (dashboards, tables), full. Default `xl`.
- padding?: 'none' | 'sm' | 'md' | 'lg' = 'md'. Horizontal padding from the space scale: none, sm (16), md (24), lg (32). Default `md`.
- render?: ReactElement. Render another element, for example `<main />`.
When to use:
- The outer wrapper of a page or a page section.
- sm for forms and reading (keeps lines under about 75 characters), lg for settings, xl for dashboards and tables.
- render={<main />} on the page's main region.
When not to use:
- Inside cards or dialogs: they already set their width and padding.
- Full-bleed backgrounds: put the background on an outer element and the Container inside it.
Do:
- Match the width to the content. Forms in an xl container stretch inputs to unreadable lengths. Pick sm.
Don't:
- Nested containers. A container inside a container doubles the padding and adds nothing.
Accessibility:
- A plain div by default; render as <main /> for the main region so the landmark exists.
- Content reflows at 320px wide without horizontal scroll (WCAG 1.4.10).
Tokens used: --size-160, --size-180, --size-240, --size-300, --space-4, --space-6, --space-8
Docs facts: [/docs-ai/container.json](/docs-ai/container.json)
### Grid
Two-dimensional layout with token gaps: a fixed number of equal columns, or as many columns as fit for card and tile grids.
Import: `import { Grid } from '@bridge-ds/react'`
Accepts all native <div> attributes.
Props:
- columns?: number = 1. Fixed number of equal columns (1 to 12). Ignored when minItemWidth is set. Default 1.
- minItemWidth?: '60' | '72' | '80' | '96' | '120'. Responsive columns: as many as fit, each at least this size token wide (`80` = 320px). Use this instead of breakpoints for card and tile grids.
- gap?: SpaceToken. Space between cells, a key of the space scale. Default: the density token `layout.gap`.
- render?: ReactElement. Render another element, for example `<ul />`.
When to use:
- Card and tile grids: minItemWidth makes them responsive without breakpoints.
- Dashboards and settings layouts with a fixed column count.
- render={<ul />} when the cells are a list of items.
When not to use:
- Tabular data with headers: use Table.
- A single row or column: use Inline or Stack.
Do:
- Prefer minItemWidth for tiles. Tiles reflow from four columns to one as space shrinks, with no media queries.
Don't:
- Many fixed columns on small screens. Twelve fixed columns at phone width make unreadable slivers. Use minItemWidth or fewer columns.
Accessibility:
- A plain div by default with no role; it is not a data grid. Use Table for data.
- Cells flow in DOM order, so reading and tab order follow the visual order row by row.
Tokens used: --layout-gap
Docs facts: [/docs-ai/grid.json](/docs-ai/grid.json)
### Inline
Lays out children in a row with a token gap and wraps them when space runs out: badges, button rows, metadata.
Import: `import { Inline } from '@bridge-ds/react'`
Accepts all native <div> attributes.
Props:
- gap?: SpaceToken = '2'. Space between children, a key of the space scale. Default `2` (8px).
- align?: FlexAlign = 'center'. Cross-axis alignment. Default `center`.
- justify?: FlexJustify = 'start'. Main-axis distribution. Default `start`.
- wrap?: boolean = true. Wrap onto new lines when space runs out. Default true.
- render?: ReactElement. Render another element, for example `<ul />`.
When to use:
- Rows of related small items: badges, tags, buttons, meta text.
- justify="between" for page and card headers: title at the start, actions at the end.
- wrap={false} only when the row must stay on one line and can scroll or truncate.
When not to use:
- A vertical list: use Stack.
- Columns that must line up across rows: use Grid or Table.
Do:
- Let rows wrap. Wrapping keeps every item reachable at narrow widths and at 200% zoom.
Don't:
- Fixed widths inside an Inline. Fixed widths fight the wrap and overflow on phones. Let content size the items.
Accessibility:
- A plain div by default; use render for a list (<ul />) when the items are a list.
- Wrapping never reorders items, so the visual and tab order match.
Tokens used: --space-2
Docs facts: [/docs-ai/inline.json](/docs-ai/inline.json)
### PageHeader
The top of a page: the page title, a one-line lead that says what the page is for, an optional meta line (freshness, counts) and up to three page actions.
Import: `import { PageHeader } from '@bridge-ds/react'`
Props:
- title: ReactNode. The page name, as in the navigation: "Spacing", "Install".
- lead?: ReactNode. One or two sentences under the title: what this page is for. Falls back to `children`.
- meta?: ReactNode. Small muted line under the lead: freshness, counts, owner.
- actions?: ReactNode. One to three page-level actions, usually a primary or secondary Button, or links styled as buttons.
- headingLevel?: 1 | 2 = 1. Heading level of the title. Default 1: one PageHeader per page.
- children?: ReactNode. Lead content when `lead` is not set (handy from templates that pass content as children).
When to use:
- Once at the top of every page, so titles, leads and actions sit in the same place across the product.
- lead: one or two sentences on what the page is for, not how to use it.
- meta: a quiet line for facts that build trust, like when the page was last updated or where its data comes from.
- actions: the page-level actions (Edit in Studio, Create token). Keep the primary one last.
- headingLevel 2 when the header tops a section inside a page that already has an h1.
When not to use:
- Section titles inside a page: use a plain h2 heading.
- Dialogs and drawers: they have their own title slot.
- Promotional heroes with art and several calls to action: build them in the page, outside the system.
Do:
- Name the page as the navigation does. The title repeats the sidebar label, so users know they landed where they clicked.
- Keep the lead to one idea. One or two sentences that say what the page is for. Details belong in the content.
Don't:
- A paragraph as the lead. A long lead pushes the content below the fold and nobody reads it.
- Title in all caps or title case. Sentence case reads faster and matches the rest of the interface.
Accessibility:
- Renders a header element with a real heading (h1 by default), so the page outline and screen reader heading navigation start here.
- One h1 per page: use headingLevel 2 when the page already has one.
- Actions keep their own roles: a Button for actions on this page, a link (Button with render={<a />}) for navigation.
- The meta line is plain text in the muted color, which passes AA on every surface.
Tokens used: --color-text-default, --color-text-muted, --font-family-sans, --font-line-height-normal, --font-size-100, --font-tracking-wide, --size-120, --size-8, --space-2, --space-3, --space-4, --space-6, --space-8, --typography-body-line-height, --typography-body-size, --typography-h1-line-height, --typography-h1-size, --typography-h1-tracking, --typography-h1-weight, --typography-h2-family, --typography-h2-line-height, --typography-h2-size, --typography-h2-tracking, --typography-h2-weight, --typography-h3-family, --typography-h3-line-height, --typography-h3-size, --typography-h3-tracking, --typography-h3-weight
Docs facts: [/docs-ai/page-header.json](/docs-ai/page-header.json)
### Stack
Stacks children in a column with a gap from the space scale. The default gap follows density (16px comfortable, 12px compact).
Import: `import { Stack, gapStyle } from '@bridge-ds/react'`
Accepts all native <div> attributes.
Props:
- gap?: '0' | '0-5' | '1' | '1-5' | '2' | '3' | '4' | '5' | '6' | '8' | '10' | '12' | '16'. Space between children, a key of the space scale. Default: the density token `layout.gap` (16 or 12px).
- align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline' = 'stretch'. Cross-axis alignment. Default `stretch`.
- justify?: 'start' | 'center' | 'end' | 'between' = 'start'. Main-axis distribution. Default `start`.
- render?: ReactElement. Render another element, for example `<section />` or `<ul />`.
When to use:
- Vertical rhythm inside cards, dialogs, forms and page sections.
- gap from the space scale: tight (1 to 2) inside a group, generous (6 to 8) between groups.
- render to output a semantic element: <section />, <ul />, <form />.
When not to use:
- Rows of items: use Inline.
- Two-dimensional layouts with columns: use Grid.
- Spacing one element only: give that element a margin class in your app, not a wrapper.
Do:
- Nest stacks to show grouping. A tight stack per group inside a looser stack: the space itself says what belongs together.
Don't:
- One gap for everything. Equal spacing everywhere flattens hierarchy. Vary the gap to separate groups.
Accessibility:
- A plain div by default with no role: it changes layout, not semantics.
- Use render to give the stack the right element (a list of items should be <ul />).
- Visual order equals DOM order, so reading and tab order stay consistent.
Tokens used: --layout-gap
Docs facts: [/docs-ai/stack.json](/docs-ai/stack.json)
## Optional
- [AGENTS.md](https://github.com/the-bridge-company/ai-design-system-demo/blob/main/AGENTS.md): full agent instructions
- [GOVERNANCE.md](https://github.com/the-bridge-company/ai-design-system-demo/blob/main/docs/GOVERNANCE.md): risk tiers and review gates
- [Token sources](https://github.com/the-bridge-company/ai-design-system-demo/tree/main/packages/tokens/src): DTCG JSON
MCP server
Tokens resolved in every brand, theme and density, the contrast gate, components with their props and usage docs, and the agent rules. Claude builds UI with real tokens and proposes token changes as pull requests your design and tech owners review.
1. Claude Code
Connect Claude to your design system repository on GitHub. No clone, nothing to build, and read-only until you add a token:
claude mcp add bridge -s user -- npx -y @bridge-ds/mcp --repo owner/nameTo let Claude open changes for review, add -e GITHUB_TOKEN=github_pat_... before --: a fine-grained token limited to that one repository, with contents and pull requests read and write. Without a token, GitHub allows 60 requests an hour.
2. Claude Desktop
In Settings, Developer, Edit config, add this to claude_desktop_config.json, then restart Claude Desktop:
{
"mcpServers": {
"bridge": {
"command": "npx",
"args": [
"-y",
"@bridge-ds/mcp",
"--repo",
"owner/name"
]
}
}
}For the token, add "env": { "GITHUB_TOKEN": "github_pat_..." } next to args.
Work on a local folder instead of a GitHub repository
Pass --root with the path to your design system checkout. Claude Code:
claude mcp add bridge -- npx -y @bridge-ds/mcp --root /abs/path/to/your-dsOr share it with your team through git: a project .mcp.json at the repository root. Claude Code asks you to approve project servers the first time.
{
"mcpServers": {
"bridge": {
"command": "npx",
"args": [
"-y",
"@bridge-ds/mcp",
"--root",
"."
]
}
}
}Claude Desktop:
{
"mcpServers": {
"bridge": {
"command": "npx",
"args": [
"-y",
"@bridge-ds/mcp",
"--root",
"/abs/path/to/your-ds"
]
}
}
}Other MCP clients work the same way over stdio: npx -y @bridge-ds/mcp --repo owner/name (add --ref for another branch) or --root <dir>. Options can also come from BRIDGE_REPO, BRIDGE_REF, BRIDGE_ROOT and GITHUB_TOKEN.
What Claude can do: 68 tools
| Tool | Use it to |
|---|---|
bridge_get_config | See where the design system is, which modes exist, and what Bridge may write |
bridge_list_tokens | Browse tokens by tier (primitive, brand, semantic, density, component), type, prefix, mode |
bridge_get_token | Get a token's value in every mode, its alias chain, what aliases it and which components use it |
bridge_search_tokens | Find the right token by intent ("danger text"), CSS variable or value (#c2410c, 16px) |
bridge_check_contrast | Run the WCAG contrast gate; preview the effect of edits before proposing them |
bridge_validate_tokens | Check DTCG shapes, aliases (broken, circular), type mismatches, light and dark parity |
bridge_list_components | Pick an existing component before writing new markup |
bridge_get_component | Read props (types, defaults, allowed values), when to use, dos and donts, accessibility, anatomy |
bridge_scaffold_component | Generate a component folder that follows the repo checklist |
bridge_draft_docs | Start or refresh a component's docs.json: what the code tells (props, tokens, anatomy, contrast, changelog line) plus a prompt for you to write the prose (see Docs drafts) |
bridge_get_design_language | Read the relevant section of the design language |
bridge_get_guidelines | Read the agent rules (always, ask before, never) and the risk tiers |
bridge_propose_token_change | Turn token edits into a suggestion (an issue a developer applies, the default) or, in direct mode, a branch (local) or a draft PR (GitHub), with a designer-friendly body, for a product and with a jira key; with branch, commit them to an exploration; with pr, add them to a branch already in review |
bridge_list_brands | See the brands: slug, name, default, primary action color, theme overrides |
bridge_plan_brand | Dry run of a new brand: generated ramps, every contrast pair in light and dark, files, tier, PR body |
bridge_propose_brand | Turn that plan into a bridge/brand-<slug> branch (local) or a draft PR (GitHub), never main |
bridge_list_reviews | List open PRs and local proposal branches |
bridge_get_review | Review a token PR: diff table, contrast before and after, downstream impact, tier, CI, approvals |
bridge_review_pr | Comment, approve or request changes on a PR, as you (kept for older prompts; bridge_review_branch is the designer version) |
bridge_visual_impact | See what a change does on screen: before/after/diff of the impacted stories and app screens |
bridge_get_history | See what changed in the tokens: versions, merged PRs and commits, who, when, why, tier, breaking |
bridge_get_token_history | Every change of one token: value before and after per mode, the PR and why, renames followed |
bridge_plan_revert | Dry run of undoing a change (or restoring one token) against today's base branch, conflicts shown |
bridge_propose_revert | Turn that plan into a branch (local) or a draft PR labelled revert (GitHub), never main |
bridge_audit | Measure how a product codebase adopts the design system (CSS, SCSS, Less, TS/JS, TSX/JSX, Astro, Vue, Svelte): token and component adoption, hardcoded values with the closest token, foreign UI components and native elements with the DS component to use, top files and fixes, and what was ignored on purpose with its reason |
bridge_list_requests | List component change requests (GitHub issues labeled component-change) with status: requested, in progress (an open pull request says "Closes #n"), done, declined |
bridge_get_request | Read one request with the component's files, the agent rules and a brief to implement it in a pull request that closes it |
bridge_create_request | File a request as an issue labeled component-change (source: mcp); needs GITHUB_TOKEN; dryRun previews it; exploration implements it on an exploration branch |
bridge_start_exploration | Start an exploration: an explore/<name> branch and a draft pull request labeled exploration, with previews and a snapshot package for a product staging branch, never merged as is (see Explorations) |
bridge_list_explorations | List explorations: purpose, status (running, waiting for code, being built, shipped, closed), archived ones on demand, previews, the snapshot install command and the requests implemented on each |
bridge_promote_exploration | Done exploring (old name kept): the picked pieces become requests for the DS lead, the branch goes to the developers; never closes, merges or marks it ready |
bridge_archive_exploration | Archive or unarchive an exploration: hidden from the lists, its pull request and preview still open |
bridge_close_exploration | Close an exploration without merging it (removes its preview: prefer archive) |
bridge_missing_pieces | What a product branch adds that the library lacks (the audit on its diff against main): variants of library components, new components, token values; record saves them where Studio shows them |
bridge_open_library_branch | Open the library side of one piece, linked both ways: a token change (and the product branch takes its preview), a library exploration for a variant, or a component request |
bridge_product_branch | A product branch as Studio shows it: the summary, each linked library branch with its status, reviewers, preview and next action |
bridge_use_library_preview | The product branch takes the latest library branch of a piece (merged in), so its preview shows the real library change |
bridge_switch_to_published | Once the library shipped, bring main into the product branch so it uses the published version |
bridge_figma_changes | The library branches the DS Bridge plugin opened for components changed in Figma (label from-figma): exact token changes, and branches waiting for Claude AI or drafted by Claude AI |
bridge_get_figma_change | One of them: its spec (variant matrix, properties, bound variables and layers per variant, exact token swaps) and the brief to draft the code on its branch |
bridge_mark_figma_change_drafted | Once the code is pushed to that branch: mark the pull request ready for review and label it drafted-by-claude, in one step, for a person to review (refused while the branch only carries the spec; when GitHub refuses ready for review, the labels stay as they were) |
bridge_push_figma_component | The plugin's push from a structured component diff: an exact token change (contrast gated), a draft branch with the spec for Claude AI, or what to fix in Figma; dryRun shows the route |
bridge_list_suggestions | List suggestions (issues labeled bridge-suggestion): kind, tier, status (needs review, changes requested, approved, being applied, applied, declined, outdated) and linked pull requests |
bridge_get_suggestion | Read one: the change, the checks when suggested, its version, who approved it and which approvals are missing, and for a component spec the brief to implement it |
bridge_create_suggestion | File any change as a suggestion from its payload (bridge.suggestion/1); paths checked against the kind's folders and the guardrails first; dryRun previews it |
bridge_review_suggestion | Approve, request changes, comment or decline, as the token's owner; counts only for owners, on the version you read (payloadSha) |
bridge_apply_suggestion | "Apply Bridge suggestion #42": re-applies it on today's code and re-runs the checks; dry-run first, then a new local branch (default), the working tree (write) or a patch; on a remote repo, the files to write |
bridge_link_suggestion | Record the pull request that applies a suggestion (it must say Closes #n) |
bridge_list_products | See the products (products in bridge.config.json) and the one the current folder or repo belongs to |
bridge_what_needs_me | The review queue as Studio counts it, with the branches where you act now first |
bridge_branch_status | One branch: product, status, approvals, tests, how it stands against main, whether you may merge it, next step |
bridge_list_comments | The conversation on a branch and its line threads, with the ids to reply to |
bridge_send_for_review | Send a draft branch for review (its author) |
bridge_back_to_draft | Take a branch back to draft (its author) |
bridge_review_branch | Approve, request changes or comment, on the version you saw |
bridge_ask_someone_else | Ask the other people who can give the missing approvals to review (the review queue action) |
bridge_comment | Comment on a branch, or reply in a line thread |
bridge_merge_branch | Merge through the same gate as Studio's Merge button; owner override with a reason posted first |
bridge_close_branch | Close a branch without merging it |
bridge_reopen_branch | Reopen a closed branch (its author or an owner, while its work is still there) |
bridge_update_from_main | Bring the latest main into a branch; token conflicts come back to pick ("mine" or "theirs") |
bridge_try_branch | Try it in a product (the preview Vercel reported) and share it with a product team (install command, message) |
bridge_combine_branches | Try 2 to 4 token branches together: conflicts to pick, links to open the combination |
bridge_save_docs | Save a component's usage guidance on a new branch sent for review, or on an open branch |
bridge_get_settings | Studio settings that are safe to read: roles, who ships, owner override, protection, products |
bridge_figma_coverage | Which components are in Figma, and whether code images exist for the fidelity check |
bridge_pending_drafts | Read the change texts a designer asked you to write from the Figma plugin ("Draft with my Claude"): the changes, the starting draft, their notes, the voice rules, the output contract, and the before and after images as image content (see Change texts from Figma) |
bridge_write_drafts | Write the titles, summaries and change lines back; the plugin fills them in within seconds for the designer to review |
Prompts
Slash commands in Claude Code, for example /mcp__bridge__audit-ui-for-tokens.
review-token-pr: review a token PR like the design system owner, then post only after you confirm.add-component: check for an existing component, plan the API and tokens, then scaffold.audit-ui-for-tokens: paste CSS or TSX; Bridge flags hardcoded colors, lengths, durations, shadows and z-index and suggests the token for each, matched to the property (a background gets a surface token).migrate-file-to-ds: give a product file; Bridge audits it (values and components with line numbers), then Claude rewrites it with DS components and tokens, explains each change and re-runs the audit.
Things to ask
- Which token should this card background use in dark mode?
- Darken muted text in the light theme and show me the contrast impact first.
- Review the newest open change for the design owner.
- Audit this component's CSS for hardcoded values.
bridge CLI
The same checks and proposals from a terminal or CI, from npm with nothing to install first. Run it in your design system repository: every command below is npx @bridge-ds/cli <command>, starting with check.
npx @bridge-ds/cli checkbridge init- Detects your token folder, tiers and modes, writes bridge.config.json and .mcp.json, prints the Claude Desktop snippet.
bridge check- Validates tokens and runs the contrast gate. Use it in CI: exit 0 ok, 1 failures, 2 setup error.
bridge tokens <query>- Searches tokens by words, CSS variable or value.
bridge diff <base>- Designer-readable token diff against a git ref, with alias impact, contrast and the risk tier.
bridge propose --title "..." --set path=value- Turns token edits into a commit on a new local bridge/* branch. Blocked when a check fails.
bridge history [token]- Token history: versions and merged changes, or every change of one token, with who, when and why.
bridge revert <pr|commit> --dry-run- Plans the undo of a merged token change against today: conflicts with later changes shown. Without --dry-run, a new local branch.
bridge impact --plan- Visual impact: which stories and app screens a change touches. Without --plan, screenshots both refs and pixel-diffs them (needs Chrome).
bridge audit <path>- Design system adoption in product code (CSS, TSX, Astro, Vue, Svelte): token and component adoption, hardcoded values with the closest token, native elements to replace, top files. --fix for exact CSS matches.
bridge mcp- Starts the MCP server on stdio, same as npx @bridge-ds/mcp.
Safety model
Read-only by default. Without
GITHUB_TOKENnothing remote is written. Locally, read tools never write.Never `main`. Proposals go to new branches named
bridge/<title>, or to an exploration branchexplore/<name>when you pass one (its draft pull request is never merged as is: the check "Exploration not merged as is" fails while theexplorationlabel is on). The protected list (main,master, the configured base branch) is checked in the GitHub client and the local git writer, whatever the caller asks. Existing branches are never overwritten; a new suffix is used instead.Pull requests, not pushes. Remote proposals open a draft PR labelled
source: mcpandtier: N. Local proposals commit to a local branch through git plumbing: your checkout, index and working tree are untouched, and nothing is pushed.Reverts are planned, not blind. A revert is computed against today's base branch; tokens that changed again since are shown as conflicts and kept unless you choose otherwise.
Gates before writes. A proposal that adds a validation error or makes a contrast pair fail is blocked and nothing is written.
Tiers. Any token added, changed, renamed or removed is Tier 3 (design and tech owner approval), as in
docs/GOVERNANCE.md. Renames and removals aremajor; a description-only edit is Tier 1,patch. The Figma plugin, the docs site and the MCP server use the same classifier. Bridge computes the tier from the diff, writes the changeset, and warns in reviews when a PR is labelled lower than its real tier.Merges pass the Studio gate, and only when you ask.
bridge_merge_branchmerges only what Studio's Merge button would (tests pass, the approvals the tier needs, no changes requested, no conflict, the right to ship), under your GitHub account; the owner override skips missing approvals only, with your reason posted on the branch first. Reviews, comments and closes are explicit tools too, never side effects of a read.Suggestions write issues, not code. In suggest mode the writing tools only create issues; applying one checks its paths, the owners' approvals on its current version and the gates on today's code, and commits to a new local branch (never pushes).
Tokens. Use a fine-grained GitHub token limited to the one repo: Contents read and Issues read and write is enough for suggestions; direct mode needs contents and pull requests read and write. Bridge sends it only to
api.github.com.
Full guide: docs/AI.md. Source and license: GitHub, MIT.