# 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/.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/. 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 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