Spec sheet

Every token value and every component style binding for the calm-precise theme, resolved in light and dark, with contrast ratios for the color pairs that matter.

Generated from the built tokens of the calm-precise theme and generated/components.json. Every value is the resolved light and dark value a component receives on every platform. Other built themes: warm-friendly, warm-sleek — regenerate for one of them with pnpm spec-sheet --theme <id>.

Spacing scale

Bars are drawn at the token’s pixel value (wide values clip to the column). Components bind to the numbered steps or the sm/md/lg aliases; layout rhythm below is what goes between components.

Steps

space.0
0px
space.1
4px
space.2
8px
space.3
12px
space.4
16px
space.5
20px
space.6
24px
space.8
32px
space.10
40px
space.12
48px
space.16
64px
space.20
80px

Aliases

space.sm
8px
space.md
12px
space.lg
16px

Layout rhythm

Gaps

layout.gap.none
0px
layout.gap.tight
4px
layout.gap.normal
8px
layout.gap.loose
16px
layout.gap.section
32px

Sections

layout.section.sm
32px
layout.section.md
48px
layout.section.lg
64px

Insets

layout.inset.none
0px
layout.inset.sm
8px
layout.inset.md
12px
layout.inset.lg
16px
layout.inset.xl
32px

Gutters

layout.gutter.narrow
16px
layout.gutter
24px
layout.gutter.wide
32px

Max widths

layout.maxWidth.prose
572px
layout.maxWidth.content
960px
layout.maxWidth.page
1280px

Type scale

Each size is rendered at its own value in font.family.body, at font.lineHeight.normal (1.5). Line-height tokens: tight 1.2, normal 1.5, loose 1.7.

The quick brown fox jumps over the lazy dog
font.size.xs 11px · line height: font.lineHeight.tight / font.lineHeight.normal / font.lineHeight.loose
The quick brown fox jumps over the lazy dog
font.size.sm 13px · line height: font.lineHeight.tight / font.lineHeight.normal / font.lineHeight.loose
The quick brown fox jumps over the lazy dog
font.size.md 16px · line height: font.lineHeight.tight / font.lineHeight.normal / font.lineHeight.loose
The quick brown fox jumps over the lazy dog
font.size.lg 19px · line height: font.lineHeight.tight / font.lineHeight.normal / font.lineHeight.loose
The quick brown fox jumps over the lazy dog
font.size.xl 23px · line height: font.lineHeight.tight / font.lineHeight.normal / font.lineHeight.loose
The quick brown fox jumps over the lazy dog
font.size.2xl 28px · line height: font.lineHeight.tight / font.lineHeight.normal / font.lineHeight.loose
The quick brown fox jumps over the lazy dog
font.size.3xl 33px · line height: font.lineHeight.tight / font.lineHeight.normal / font.lineHeight.loose
The quick brown fox jumps over the lazy dog
font.size.4xl 40px · line height: font.lineHeight.tight / font.lineHeight.normal / font.lineHeight.loose

Families and weights

TokenValue
font.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif
font.family.headingsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif
font.family.monoui-monospace, SFMono-Regular, Menlo, Consolas, monospace
font.weight.regular400
font.weight.medium500
font.weight.semibold600
font.weight.bold700
font.lineHeight.tight1.2
font.lineHeight.normal1.5
font.lineHeight.loose1.7

Color roles

Semantic colors only (the color.palette.* ramps are raw material components never reference). The contrast column checks text pairs against WCAG AA 4.5:1 and icon/border/focus pairs against 3.0:1; a transparent background is checked against color.background.

TokenLightDarkContrast
color.foreground #2a2b2f #ecedeeon color.background (needs 4.5): light 14.14:1 AA pass; dark 15.14:1 AA pass
color.foreground.strong #000000 #ffffffon color.background (needs 4.5): light 21.00:1 AA pass; dark 17.75:1 AA pass
color.foreground.muted #56585c #bcbec1on color.background (needs 4.5): light 7.13:1 AA pass; dark 9.53:1 AA pass
color.foreground.onAction #ffffff #ffffff—
color.foreground.danger #b51d26 #ff958don color.background (needs 4.5): light 6.63:1 AA pass; dark 8.40:1 AA pass
color.background #ffffff #17181b—
color.background.subtle #f6f7f8 #2a2b2f—
color.background.strong #ecedee #414246—
color.border #d9dbdd #414246—
color.border.strong #939599 #707176—
color.border.focus #496ded #99b5ffon color.background (needs 3.0): light 4.47:1 AA pass; dark 8.80:1 AA pass
color.border.danger #b51d26 #ff958d—
color.link #3553d2 #99b5ffon color.background (needs 4.5): light 6.33:1 AA pass; dark 8.80:1 AA pass
color.link.hover #243ab3 #c2d4ff—
color.link.visited #162391 #dee8ff—
color.control.background #ffffff #2a2b2f—
color.control.border #939599 #707176—
color.control.selectedBackground #3553d2 #496ded—
color.control.selectedForeground #ffffff #000000on color.control.selectedBackground (needs 4.5): light 6.33:1 AA pass; dark 4.69:1 AA pass
color.control.trackOff #939599 #707176—
color.status.info.background #f1f5ff #101d53—
color.status.info.foreground #405bb6 #c2d4ffon color.status.info.background (needs 4.5): light 5.68:1 AA pass; dark 10.70:1 AA pass
color.status.info.border #99b5ff #2d4597on color.status.info.background (needs 3.0): light 1.85:1 AA fail; dark 1.82:1 AA fail
color.status.info.icon #405bb6 #99b5ffon color.status.info.background (needs 3.0): light 5.68:1 AA pass; dark 7.87:1 AA pass
color.status.success.background #e0ffe0 #002e05—
color.status.success.foreground #00791d #a0eaa2on color.status.success.background (needs 4.5): light 5.21:1 AA pass; dark 10.61:1 AA pass
color.status.success.border #76d07a #005e14on color.status.success.background (needs 3.0): light 1.77:1 AA fail; dark 1.87:1 AA fail
color.status.success.icon #00791d #76d07aon color.status.success.background (needs 3.0): light 5.21:1 AA pass; dark 7.95:1 AA pass
color.status.warning.background #fff4e1 #322000—
color.status.warning.foreground #825b00 #fccc78on color.status.warning.background (needs 4.5): light 5.60:1 AA pass; dark 10.47:1 AA pass
color.status.warning.border #e6ac3f #664700on color.status.warning.background (needs 3.0): light 1.87:1 AA fail; dark 1.84:1 AA fail
color.status.warning.icon #825b00 #e6ac3fon color.status.warning.background (needs 3.0): light 5.60:1 AA pass; dark 7.70:1 AA pass
color.status.danger.background #fff2f0 #4c0005—
color.status.danger.foreground #b51d26 #ffc3bdon color.status.danger.background (needs 4.5): light 6.06:1 AA pass; dark 10.53:1 AA pass
color.status.danger.border #ff958d #940014on color.status.danger.background (needs 3.0): light 1.93:1 AA fail; dark 1.73:1 AA fail
color.status.danger.icon #b51d26 #ff958don color.status.danger.background (needs 3.0): light 6.06:1 AA pass; dark 7.59:1 AA pass
color.overlay.scrim #00000066 #00000099—
color.overlay.surface #ffffff #2a2b2f—
color.inverse.surface #17181b #ecedee—
color.inverse.foreground #f6f7f8 #17181bon color.inverse.surface (needs 4.5): light 16.55:1 AA pass; dark 15.14:1 AA pass
color.inverse.muted #bcbec1 #56585con color.inverse.surface (needs 4.5): light 9.53:1 AA pass; dark 6.08:1 AA pass
color.inverse.link #99b5ff #243ab3on color.inverse.surface (needs 4.5): light 8.80:1 AA pass; dark 7.68:1 AA pass
color.inverse.focus #99b5ff #243ab3on color.inverse.surface (needs 3.0): light 8.80:1 AA pass; dark 7.68:1 AA pass
color.inverse.status.neutral #f6f7f8 #17181bon color.inverse.surface (needs 3.0): light 16.55:1 AA pass; dark 15.14:1 AA pass
color.inverse.status.info #99b5ff #405bb6on color.inverse.surface (needs 3.0): light 8.80:1 AA pass; dark 5.28:1 AA pass
color.inverse.status.success #76d07a #00791don color.inverse.surface (needs 3.0): light 9.36:1 AA pass; dark 4.77:1 AA pass
color.inverse.status.warning #e6ac3f #825b00on color.inverse.surface (needs 3.0): light 8.74:1 AA pass; dark 5.20:1 AA pass
color.inverse.status.danger #ff958d #b51d26on color.inverse.surface (needs 3.0): light 8.40:1 AA pass; dark 5.65:1 AA pass
color.action.primary.background #3553d2 #3553d2—
color.action.primary.backgroundHover #243ab3 #496ded—
color.action.primary.foreground #ffffff #ffffffon color.action.primary.background (needs 4.5): light 6.33:1 AA pass; dark 6.33:1 AA pass
color.action.secondary.background #ecedee #414246—
color.action.secondary.backgroundHover #d9dbdd #56585c—
color.action.secondary.foreground #000000 #ffffffon color.action.secondary.background (needs 4.5): light 17.91:1 AA pass; dark 10.04:1 AA pass
color.action.ghost.background transparent transparent—
color.action.ghost.backgroundHover #f6f7f8 #2a2b2f—
color.action.ghost.foreground #3553d2 #99b5ffon color.action.ghost.background (needs 4.5): light 6.33:1 AA pass; dark 8.80:1 AA pass
color.action.danger.background #b51d26 #d33a3c—
color.action.danger.backgroundHover #940014 #b51d26—
color.action.danger.foreground #ffffff #ffffffon color.action.danger.background (needs 4.5): light 6.63:1 AA pass; dark 4.73:1 AA pass

Components

Every style binding per component, in generated/components.json order. Bindings with a {prop} slot are listed once per enum value. Locked bindings cannot be overridden per instance.

Accordion

Component doc

BindingTokenLightDarkLockedDescription
dividercolor.border #d9dbdd #414246noPassed to each composed Divider as its color override (always, even when it equals Divider’s default); Accordion writes no divider rule of its own. The Divider keeps its default spacing: none: itemGap is the only space around it, and its default semantic: false, so the dividers are decorative and the accordion reads as a list of headings. Like dividerWidth and fontFamily, this is an overridable hook even though it names no part: all seven bindings are overridable, whether or not they carry one.
dividerWidthborder.width.thin1px1pxnoPassed to each composed Divider as its thickness override.
itemGaplayout.gap.none0px0pxnopart list: Items touch; the divider separates them. Applied as the gap of the list container (flex gap on web and Lit, the gap style on React Native), never on the Disclosure itself, so when divided the gap also falls between each item and its Divider — intended: the divider sits centred in the space between items. At the default layout.gap.none that space is zero, so the centring is only observable through an itemGap override.
triggerPaddingBlockspace.md12px12pxnopart trigger: Roomier than a lone Disclosure, since accordion triggers are section headings. Forwarded to Disclosure’s triggerPaddingBlock, like fontFamily, triggerFontSize and triggerFontWeight (see composition); the accordion does not style the trigger itself.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart trigger: Forwarded to Disclosure’s triggerFontFamily. Like divider and dividerWidth, it is an overridable hook even though Accordion writes no rule for it.
triggerFontSizefont.size.md16px16pxnopart trigger.
triggerFontWeightfont.weight.medium500500nopart trigger.
minTargetsize.target.min24px24pxyesThe composed Disclosure’s own minimum, applied by Disclosure; Accordion adds no rule for it (nor for focusRing/focusRingWidth). The triggerPaddingBlock forward makes the row roomier but does not guarantee a 44px target.
focusRingcolor.border.focus #496ded #99b5ffyesApplied by the composed Disclosure’s trigger; Accordion adds no rule.
focusRingWidthborder.width.focus2px2pxyesApplied by the composed Disclosure’s trigger; Accordion adds no rule.

ActionSheet

Component doc

BindingTokenLightDarkLockedDescription
scrimcolor.overlay.scrim #00000066 #00000099nopart scrim.
surfacecolor.overlay.surface #ffffff #2a2b2fyespart surface.
shadowshadow.overlay0px 8px 24px 0px #0000002e0px 8px 24px 0px #00000080nopart surface.
radiusradius.lg6px6pxnopart surface: Top corners only on the sheet.
itemPaddingBlockspace.sm8px8pxnopart item.
itemPaddingInlinelayout.inset.md12px12pxnopart item: Inline padding of the rows, and also of the header and the cancel row, which have no inline binding of their own.
itemGaplayout.gap.normal8px8pxnopart item: Between icon and label. Rows have no gap between them: their rhythm comes from itemPaddingBlock.
headerPaddingBlockspace.sm8px8pxnopart header: Vertical padding of the header (handle + heading) and of the cancel row; their inline padding is itemPaddingInline. When no header is rendered (not dismissible and no heading) nothing compensates for it: the first row’s own itemPaddingBlock is the top spacing.
headerGaplayout.gap.tight4px4pxnopart header: Between the handle and the heading.
handlecolor.foreground.muted #56585c #bcbec1yespart handle: A pill (space.1 tall, space.10 wide) centered in the header, decorative and hidden from assistive technology, as BottomSheet.
handleHeightspace.14px4pxnopart handle.
handleWidthspace.1040px40pxnopart handle.
handleRadiusradius.full999px999pxnopart handle.
itemHovercolor.background.subtle #f6f7f8 #2a2b2fyespart item, state hover: Paint only: hovering a row does not move focus, which stays on the roving item. React Native has no hover on touch, so the pressed state paints it too (react-native-web also gets hover in/out). The paint change has no binding of its own: on web and Lit it transitions over motion.duration.fast with motion.easing.standard, removed under reduced motion.
itemColorcolor.foreground #2a2b2f #ecedeeyespart item.
itemDangerColorcolor.foreground.danger #b51d26 #ff958dyespart item.
titleColorcolor.foreground.muted #56585c #bcbec1yespart heading: Realized by the composed heading Text’s tone=“muted”; Text owns its color, so this is not overridable.
titleSizefont.size.sm13px13pxnopart heading: The composed heading Text’s size=“sm”, forwarded to its fontSize override; no host hook of its own. The forward always reaches Text with ActionSheet’s resolved value: on web and Lit through Text’s CSS hook (--ds-text-font-size) set to --ds-action-sheet-title-size in the sheet’s stylesheet, with overrides.fontSize passed only when the caller set this override, so consumer CSS on the sheet hook still works; on rn the resolved value (the default token or the override) is always passed in Text’s overrides. Text sets that same hook on that same element from its own size rule, so the sheet’s declaration has to out-specify it and must target the composed Text node from the sheet’s stylesheet: on web data-part="heading" goes through Text’s rest spread and is selected with a two-class descendant selector, on Lit the outer-tree rule .heading ds-text beats Text’s own :host([size]).
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart item: Applied to each row (not the list) and forwarded to the composed heading Text as an override, since Text always sets its own family; delivered the way titleSize describes.
fontSizefont.size.md16px16pxnopart item.
itemIconSizefont.size.md16px16pxnopart itemIcon: Size of the row glyph, forwarded to the composed Icon as its size override so the glyph tracks the row’s type instead of keeping Icon’s own default; delivered the way titleSize describes.
lineHeightfont.lineHeight.normal1.51.5nopart item: Applied to each row and forwarded to the composed heading Text as an override, as fontFamily is (delivered the way titleSize describes).
dividercolor.border #d9dbdd #414246nopart divider: Two dividers, two rules. The danger-group divider is drawn only when there are both default and danger actions; it sits inside the menu and is exposed as role=“separator” (rn: a View with role=“separator” among the rows). The cancel divider sits above the cancel row whenever that row is rendered, outside the menu; it is decorative and hidden from assistive technology (aria-hidden; rn: importantForAccessibility=“no-hide-descendants” and accessibilityElementsHidden). Both carry the one divider part name; where a test has to address one of them they are in DOM order, the danger divider first and the cancel divider last.
dividerWidthborder.width.thin1px1pxnopart divider.
minTargetsize.target.comfortable44px44pxyes
maxWidthlayout.maxWidth.prose572px572pxyesAbove this width, present as a Menu anchored to the opener; the comparison is (width > token), so exactly the token width is the sheet (React Native has no wide presentation; see its note). The breakpoint is read from the theme token, not per instance. When the token cannot be resolved (no theme stylesheet yet, or jsdom) the sheet presentation is used; the comparison is retried while the document is still loading, so a late stylesheet corrects the presentation.
layerlayer.sheet200200nopart surface: Stacking order. It has no effect inside the browser top layer or a native Modal window; it applies to the non-top-layer fallback (position: fixed) and the rn anchor view. On web and Lit it is written on the itself rather than the surface: inside the top layer the surface cannot honour it, and the is the element the fallback positions.
entermotion.duration.base200ms200msnopart surface: Slide up from the bottom edge with motion.easing.standard, the scrim fading with the same duration and easing; instant under reduced motion. A sheet that mounts already open plays it too, as BottomSheet does.
exitmotion.duration.fast120ms120msnopart surface: Slide down with motion.easing.exit, the scrim fading with the same duration and easing. A below-threshold drag release springs back with this duration and motion.easing.standard, instant under reduced motion. One duration hook serves both; the two easings are fixed tokens chosen per case, with no hooks of their own. After a dismissing release the surface holds the released offset until the consumer’s next render: open false plays this exit from there; open still true springs back (motion.easing.standard over this duration), and an interrupted enter animation is finished by that spring-back.
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyes

Alert

Component doc

BindingTokenLightDarkLockedDescription
backgroundcolor.status.info.background #f1f5ff #101d53yespart container.
backgroundcolor.status.success.background #e0ffe0 #002e05yespart container.
backgroundcolor.status.warning.background #fff4e1 #322000yespart container.
backgroundcolor.status.danger.background #fff2f0 #4c0005yespart container.
foregroundcolor.status.info.foreground #405bb6 #c2d4ffyespart heading: Heading color.
foregroundcolor.status.success.foreground #00791d #a0eaa2yespart heading: Heading color.
foregroundcolor.status.warning.foreground #825b00 #fccc78yespart heading: Heading color.
foregroundcolor.status.danger.foreground #b51d26 #ffc3bdyespart heading: Heading color.
bodyColorcolor.foreground #2a2b2f #ecedeeyespart body: Body text keeps the page foreground so long messages read as text, not as colored emphasis. On React Native a string or number body is wrapped in the system Text, whose default tone is this color, so no color is passed; a non-string body is rendered as given. A body of Text or Link children therefore keeps its own color on every platform: this binding reaches plain text only, by inheritance on web and Lit, and inheriting a value is not restyling a child.
bordercolor.status.info.border #99b5ff #2d4597nopart container.
bordercolor.status.success.border #76d07a #005e14nopart container.
bordercolor.status.warning.border #e6ac3f #664700nopart container.
bordercolor.status.danger.border #ff958d #940014nopart container.
iconcolor.status.info.icon #405bb6 #99b5ffyespart icon: Leading icon: info circle, check circle, warning triangle, or error octagon by tone, rendered with the system Icon whose glyph name is the tone value itself (info, success, warning, danger), so a new tone needs a glyph of the same name, and colored by passing this token path as overrides.color to the Icon on every platform, React Native included (never Icon’s RN color prop) — the sanctioned way to color a composed child. Locked by its non-text contrast pair, so there is no overrides.icon and no —ds-alert-icon hook: the Icon always gets this tone token, and the icon part box carries no color of its own. Decorative: the Icon has no label, so it hides itself from assistive technology (aria-hidden; on React Native accessibilityElementsHidden and importantForAccessibility no), and the Alert-owned box adds no accessibility props on any platform precisely because the Icon already hides itself — the box must not repeat them; the tone is also conveyed by the heading or role.
iconcolor.status.success.icon #00791d #76d07ayespart icon: Leading icon: info circle, check circle, warning triangle, or error octagon by tone, rendered with the system Icon whose glyph name is the tone value itself (info, success, warning, danger), so a new tone needs a glyph of the same name, and colored by passing this token path as overrides.color to the Icon on every platform, React Native included (never Icon’s RN color prop) — the sanctioned way to color a composed child. Locked by its non-text contrast pair, so there is no overrides.icon and no —ds-alert-icon hook: the Icon always gets this tone token, and the icon part box carries no color of its own. Decorative: the Icon has no label, so it hides itself from assistive technology (aria-hidden; on React Native accessibilityElementsHidden and importantForAccessibility no), and the Alert-owned box adds no accessibility props on any platform precisely because the Icon already hides itself — the box must not repeat them; the tone is also conveyed by the heading or role.
iconcolor.status.warning.icon #825b00 #e6ac3fyespart icon: Leading icon: info circle, check circle, warning triangle, or error octagon by tone, rendered with the system Icon whose glyph name is the tone value itself (info, success, warning, danger), so a new tone needs a glyph of the same name, and colored by passing this token path as overrides.color to the Icon on every platform, React Native included (never Icon’s RN color prop) — the sanctioned way to color a composed child. Locked by its non-text contrast pair, so there is no overrides.icon and no —ds-alert-icon hook: the Icon always gets this tone token, and the icon part box carries no color of its own. Decorative: the Icon has no label, so it hides itself from assistive technology (aria-hidden; on React Native accessibilityElementsHidden and importantForAccessibility no), and the Alert-owned box adds no accessibility props on any platform precisely because the Icon already hides itself — the box must not repeat them; the tone is also conveyed by the heading or role.
iconcolor.status.danger.icon #b51d26 #ff958dyespart icon: Leading icon: info circle, check circle, warning triangle, or error octagon by tone, rendered with the system Icon whose glyph name is the tone value itself (info, success, warning, danger), so a new tone needs a glyph of the same name, and colored by passing this token path as overrides.color to the Icon on every platform, React Native included (never Icon’s RN color prop) — the sanctioned way to color a composed child. Locked by its non-text contrast pair, so there is no overrides.icon and no —ds-alert-icon hook: the Icon always gets this tone token, and the icon part box carries no color of its own. Decorative: the Icon has no label, so it hides itself from assistive technology (aria-hidden; on React Native accessibilityElementsHidden and importantForAccessibility no), and the Alert-owned box adds no accessibility props on any platform precisely because the Icon already hides itself — the box must not repeat them; the tone is also conveyed by the heading or role.
borderWidthborder.width.thin1px1pxnopart container.
radiusradius.md4px4pxnopart container.
paddingspace.md12px12pxnopart container.
gapspace.312px12pxnopart container: Horizontal gap between icon, content, and dismiss button.
partGapspace.14px4pxnopart container: Vertical gap between heading and body.
iconSizefont.size.lg19px19pxnopart icon: Forwarded to the Icon as overrides.size (a token path, default font.size.lg); Icon’s size enum is not used here. Override through overrides.iconSize; the —ds-alert-icon-size hook does not resize the Icon. The icon part is a box as tall as the first line of text — headingSize × lineHeight when there is a heading, else fontSize × lineHeight, or iconSize when that is larger — with the Icon centred in it, so the glyph lines up with the first line; that math reads the same token as the forward. overrides.iconSize is the only supported input: the component writes —ds-alert-icon-size from it and the box math reads that hook, so a consumer who sets the hook directly in their own CSS resizes the box but not the glyph, which is unsupported. Web and Lit know whether there is a heading from a data-has-heading attribute the component sets on its root — the <div> on web and the host element on Lit, where the hooks also live, not the shadow container part — and not from a CSS :has() query; all hooks (fontSize, headingSize, lineHeight, iconSize) are set on that root and inherited, so the icon box reads them there. React Native runs the same max(): the first line’s lineHeight, or iconSize when the glyph is larger.
headingSizefont.size.md16px16pxnopart heading: The heading; body text uses fontSize.
headingWeightfont.weight.semibold600600nopart heading.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart container: Inherited by heading and a string body on web and Lit; on React Native forwarded to the heading Text style and to the body Text’s overrides.fontFamily.
fontSizefont.size.md16px16pxnopart body: Body text. It reaches a string body directly (on React Native through the wrapping Text’s overrides.fontSize, with lineHeight forwarded the same way); a body composed of Text or Link children keeps its own sizing, since a composite never restyles a child.
lineHeightfont.lineHeight.normal1.51.5nopart container: Set on the root and inherited by the heading, the body and the icon-box math; the heading has no line-height binding of its own and uses this one.
dismissMarginspace.14px4pxnopart dismissButton: Negative margin-block-start and margin-inline-end only (not block-end) on the wrapper around the dismiss Button (web span, Lit span, RN View) so its target sits in the corner without enlarging the padding; the Button keeps its own colors, radius and focus ring. On React Native this is the one sanctioned sibling margin (marginTop and marginEnd of −dismissMargin on the wrapper View). Excluding block-end is a React Native rule, where the wrapper View can otherwise stretch; on web and Lit the container aligns its children to the start, so the wrapper never reaches the block-end padding and the exclusion has no visible effect.

AlertDialog

Component doc

BindingTokenLightDarkLockedDescription
scrimcolor.overlay.scrim #00000066 #00000099nopart scrim.
surfacecolor.overlay.surface #ffffff #2a2b2fyespart surface.
bordercolor.border #d9dbdd #414246no
borderWidthborder.width.thin1px1pxno
shadowshadow.overlay0px 8px 24px 0px #0000002e0px 8px 24px 0px #00000080no
radiusradius.lg6px6pxno
insetlayout.inset.lg16px16pxnoPads the inline edges of every part; the block edges are padded once, on the surface column that holds the parts (padding-block: inset), not per part, so nothing doubles. partGap is the only space between the icon-and-text row and the footer.
partGaplayout.gap.loose16px16pxnopart surface: The gap of the surface’s column, between the icon-and-text row and the footer: the icon sits inline with the text block, so this measures from that whole row.
textGaplayout.gap.tight4px4pxnoBetween title and description: the gap of the unnamed column that holds them inside the icon-and-text row, which is why it names no part. inset’s inline padding covers that row, so the icon never touches the surface edge.
iconGaplayout.gap.normal8px8pxnopart surface: Between the icon and the text block — the gap of the row inside the surface that holds both, not a property of the icon element (an icon cannot own a gap).
footerGaplayout.gap.tight4px4pxnopart footer: Forwarded to the footer Stack’s gap. The forward always reaches the Stack with AlertDialog’s resolved value: on web and Lit the AlertDialog stylesheet sets the Stack’s own --ds-stack-gap hook on the Stack element to --ds-alert-dialog-footer-gap, and overrides.gap is passed only when the caller set the footerGap override, so consumer CSS on the AlertDialog hook still works; on rn the resolved value (the token or the override) is always passed as overrides.gap. It has no hook beyond --ds-alert-dialog-footer-gap. That hook is the delivery path; the Stack’s declared gap="tight" prop resolves to the same token and is the composition’s stated default, not a second route.
iconSizefont.size.lg19px19pxnopart icon: Forwarded to the tone Icon’s size; the Icon gets no size prop. Delivered as footerGap is: on web and Lit the stylesheet sets the Icon’s --ds-icon-size hook to --ds-alert-dialog-icon-size and overrides.size is passed only when the caller set the override; on rn the resolved value is always passed as overrides.size. No hook beyond --ds-alert-dialog-icon-size.
iconcolor.status.danger.icon #b51d26 #ff958dyespart icon: Forwarded to the tone Icon’s color. It is the foreground of the non-text contrast pair and so is not a member of the overrides type; the caller never overrides it and it is always delivered: on web and Lit a per-tone rule in the AlertDialog stylesheet sets the Icon’s own --ds-icon-color hook on the Icon element (setting color on an ancestor or wrapper has no effect, because Icon’s own rule wins); on rn overrides.color is always passed. These mechanics are AlertDialog’s own: Alert and Toast reach the same result by passing overrides.color from render on every platform, and the difference is deliberate, not a drift to be copied either way.
iconcolor.status.warning.icon #825b00 #e6ac3fyespart icon: Forwarded to the tone Icon’s color. It is the foreground of the non-text contrast pair and so is not a member of the overrides type; the caller never overrides it and it is always delivered: on web and Lit a per-tone rule in the AlertDialog stylesheet sets the Icon’s own --ds-icon-color hook on the Icon element (setting color on an ancestor or wrapper has no effect, because Icon’s own rule wins); on rn overrides.color is always passed. These mechanics are AlertDialog’s own: Alert and Toast reach the same result by passing overrides.color from render on every platform, and the difference is deliberate, not a drift to be copied either way.
iconcolor.status.info.icon #405bb6 #99b5ffyespart icon: Forwarded to the tone Icon’s color. It is the foreground of the non-text contrast pair and so is not a member of the overrides type; the caller never overrides it and it is always delivered: on web and Lit a per-tone rule in the AlertDialog stylesheet sets the Icon’s own --ds-icon-color hook on the Icon element (setting color on an ancestor or wrapper has no effect, because Icon’s own rule wins); on rn overrides.color is always passed. These mechanics are AlertDialog’s own: Alert and Toast reach the same result by passing overrides.color from render on every platform, and the difference is deliberate, not a drift to be copied either way.
widthlayout.maxWidth.prose572px572pxnopart surface: Always the small size; an alert dialog with more content is a Dialog. The width sits on the surface itself: inline-size min(width, viewport width − 2 × gutter). On web and Lit the fills the viewport and centers the surface; the focusScope element only wraps it.
gutterlayout.gutter24px24pxnopart surface: The least space between the surface and each viewport edge: caps the surface width at viewport width − 2 × gutter and its max height at viewport height − 2 × gutter, both set on the surface. Content that exceeds that height cap scrolls the surface itself — the block padding and the footer scroll with it, and nothing is pinned; an alert dialog that needs a pinned footer has too much content and is a Dialog. On rn spacing is never a margin: the gutter is paddingHorizontal on the centering View, and the surface maxHeight is window height − 2 × gutter from useWindowDimensions; the sized, shadowed, animated box there is the Animated wrapper around the surface, with the surface flexShrinking inside it.
layerlayer.dialog300300noKept as the hook for consistency with Dialog, but it has no effect anywhere this component renders: web and Lit always open with showModal() into the top layer and have no non-top-layer fallback (the hook is written as z-index on the , where the top layer ignores it), and rn applies it as zIndex on the centring View, where a Modal window ignores it. An override of it is therefore a silent no-op on every platform: the binding is a forward-compatibility hook, kept so the overrides surface matches Dialog’s.
risespace.28px8pxnopart surface: The distance the surface rises during enter (translateY from rise to 0); zero under reduced motion.
entermotion.duration.base200ms200msnoOne duration shared by the scrim fade and the surface fade-and-rise (translateY of rise), with motion.easing.standard; instant under reduced motion.
exitmotion.duration.fast120ms120msnoOne duration shared by the scrim and surface fade-out, with motion.easing.exit; instant under reduced motion.
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyes

BottomSheet

Component doc

BindingTokenLightDarkLockedDescription
scrimcolor.overlay.scrim #00000066 #00000099nopart scrim.
surfacecolor.overlay.surface #ffffff #2a2b2fyespart surface.
shadowshadow.overlay0px 8px 24px 0px #0000002e0px 8px 24px 0px #00000080nopart surface.
radiusradius.lg6px6pxnoTop corners only, at every width: the surface always meets the bottom edge.
handlecolor.foreground.muted #56585c #bcbec1yespart handle: A 4×36-unit pill (space.1 tall, space.10 wide) centered in the header, decorative.
handleHeightspace.14px4pxnopart handle.
handleWidthspace.1040px40pxnopart handle.
handleRadiusradius.full999px999pxnopart handle: Rounds the handle into a pill.
headerPaddingTopspace.sm8px8pxnopart surface: Block-start padding above the handle. It is the block-start padding of the column that holds the parts (see inset) when the handle is rendered, in place of inset; it is applied once there, never as extra header padding.
handleGaplayout.gap.tight4px4pxnopart header: The header is a column of the handle over the heading row; this is that column’s gap, between the handle and the heading row.
headerGaplayout.gap.normal8px8pxnopart header: The flex gap of the heading row, between the heading and the close button, as Dialog’s headerGap. The heading row is a sheet-owned element inside the header, not an anatomy part, with no data-part or testID. With the heading hidden (hideHeading) the close button is end-aligned in the row; a header with no visible heading, no handle and no close button is not rendered, on every platform — on web and Lit the visually hidden heading then sits at the start of the column, and on rn nothing is rendered because the name is the surface’s accessibilityLabel.
insetlayout.inset.lg16px16pxnoInline padding of every part: the header and footer wrappers apply it themselves and the body Box receives it as overrides.paddingInline (its block padding stays zero). The block edges are padded once, on the column that holds header, body and footer (the surface): padding-block-start inset (headerPaddingTop when the handle is rendered) and padding-block-end inset, plus the bottom safe-area inset at every width. No part has block padding of its own, so nothing doubles.
partGaplayout.gap.loose16px16pxnoThe only space between header, body and footer: the flex gap of the column. Box never scrolls, so the body part is a Box inside a sheet-owned scroll element (unnamed, no part hook of its own); it is that wrapper the gap measures to. On rn the empty SafeAreaView that adds the bottom inset is not a part either and takes no gap: cancel the column gap above it, so the bottom clearance is exactly inset plus the safe-area inset.
footerGaplayout.gap.tight4px4pxnopart footer: Between footer actions; forwarded to the footer Stack as overrides.gap, the footer row end-aligned as in Dialog. A forward always reaches the Stack with the resolved value: on web and Lit through the Stack’s own --ds-stack-gap hook, set in the sheet’s stylesheet to --ds-bottom-sheet-footer-gap, with overrides.gap passed only when the caller set that override, so consumer CSS on the sheet’s hook still reaches the Stack — this is delivery of the forward, not restyling the child; on rn the resolved value (token or override) is always passed in overrides; inset reaches the body the same way.
maxWidthlayout.maxWidth.prose572px572pxyespart surface: The surface’s maximum inline size, centred with equal inline margins: the surface is min(viewport width, token) wide and stays anchored to the bottom edge, so on a phone it is full width and on a wide viewport it is a capped, centred sheet. It is not a breakpoint — there is one presentation at every width, never a Dialog, and nothing is decided in script. Web and Lit: max-inline-size on the surface with margin-inline: auto; rn: the surface’s maxWidth with alignSelf: center, so a tablet wider than the token gets the same capped sheet at the bottom.
layerlayer.sheet200200noHas no effect inside the browser top layer or a native Modal window; applies to the non-top-layer fallback (position: fixed) and the rn anchor view.
entermotion.duration.base200ms200msnoSlide up from the bottom edge with the scrim fading in the same duration and easing; motion.easing.standard; instant under reduced motion.
exitmotion.duration.fast120ms120msnoSlide down with motion.easing.exit; the scrim fades out in the same duration and easing. A drag dismiss plays this same transition from wherever the finger left the sheet — there is no momentum or decay physics, so no platform needs a velocity-to-animation mapping. A below-threshold release springs back to rest with this duration and motion.easing.standard (a timing animation, not a spring), instant under reduced motion.
minTargetsize.target.comfortable44px44pxyespart closeButton: Sheets are used one-handed, so the close button is raised to the comfortable target without changing Button: the closeButton part is a sheet-owned wrapper (web/Lit element with data-part="closeButton"; rn View with testID="BottomSheet.closeButton") with this minimum inline and block size, whose extra area activates the Button (web/Lit: a click on the wrapper outside the Button focuses the Button and requests close with reason close-button from the wrapper’s own click handler, never reaching into the Button’s internals or shadow root; rn: the wrapper View takes the min size and the Button’s own internally computed hitSlop — rn Button accepts no hitSlop prop, it derives one from size.target.comfortable — covers the extra area). The Button keeps its own size variant (ghost, sm, icon-only, as in Dialog) and colors.
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyes

Box

Component doc

BindingTokenLightDarkLockedDescription
paddingBlocklayout.inset.none0px0pxnoAn override applies at every value including none: layout.inset.none is a real token (a zero), not an absent part, so padding is not one of the bindings presence gates.
paddingBlocklayout.inset.sm8px8pxnoAn override applies at every value including none: layout.inset.none is a real token (a zero), not an absent part, so padding is not one of the bindings presence gates.
paddingBlocklayout.inset.md12px12pxnoAn override applies at every value including none: layout.inset.none is a real token (a zero), not an absent part, so padding is not one of the bindings presence gates.
paddingBlocklayout.inset.lg16px16pxnoAn override applies at every value including none: layout.inset.none is a real token (a zero), not an absent part, so padding is not one of the bindings presence gates.
paddingBlocklayout.inset.xl32px32pxnoAn override applies at every value including none: layout.inset.none is a real token (a zero), not an absent part, so padding is not one of the bindings presence gates.
paddingInlinelayout.inset.none0px0pxno
paddingInlinelayout.inset.sm8px8pxno
paddingInlinelayout.inset.md12px12pxno
paddingInlinelayout.inset.lg16px16pxno
paddingInlinelayout.inset.xl32px32pxno
backgroundcolor.background.none——yesnone renders the literal transparent, not a token, written out explicitly (background-color: transparent) and not read through the hook, so neither overrides.background nor consumer CSS on --ds-box-background paints a none box; the token binding covers the other three values, and the --ds-box-background hook is set only by those three, with no base default. Interpolated bindings like this one are locked — they keep their --ds-box-* hook, which is the consumer’s own-CSS escape hatch, but they are not members of the overrides type.
backgroundcolor.background #ffffff #17181byesnone renders the literal transparent, not a token, written out explicitly (background-color: transparent) and not read through the hook, so neither overrides.background nor consumer CSS on --ds-box-background paints a none box; the token binding covers the other three values, and the --ds-box-background hook is set only by those three, with no base default. Interpolated bindings like this one are locked — they keep their --ds-box-* hook, which is the consumer’s own-CSS escape hatch, but they are not members of the overrides type.
backgroundcolor.background.subtle #f6f7f8 #2a2b2fyesnone renders the literal transparent, not a token, written out explicitly (background-color: transparent) and not read through the hook, so neither overrides.background nor consumer CSS on --ds-box-background paints a none box; the token binding covers the other three values, and the --ds-box-background hook is set only by those three, with no base default. Interpolated bindings like this one are locked — they keep their --ds-box-* hook, which is the consumer’s own-CSS escape hatch, but they are not members of the overrides type.
backgroundcolor.background.strong #ecedee #414246yesnone renders the literal transparent, not a token, written out explicitly (background-color: transparent) and not read through the hook, so neither overrides.background nor consumer CSS on --ds-box-background paints a none box; the token binding covers the other three values, and the --ds-box-background hook is set only by those three, with no base default. Interpolated bindings like this one are locked — they keep their --ds-box-* hook, which is the consumer’s own-CSS escape hatch, but they are not members of the overrides type.
bordercolor.border #d9dbdd #414246noThe border colour. It shares a name with the border boolean, which decides presence: an override recolours the border and never brings one into existence. On web and Lit, without border the width is 0 and the colour hook may stay written, since nothing visible reads it; on React Native, which has no cascade, borderWidth and borderColor are left unset when border is false.
borderWidthborder.width.thin1px1pxno
radiusradius.none0px0pxnoradius: none resolves radius.none and is written out, rather than leaving the property unset — every binding is applied explicitly, with no cascade. Unlike padding, radius is presence-gated: none means no rounded corners, so overrides.radius is ignored at none and applies at every other value. As with surface: none, the none rule writes radius.none directly and reads no hook, so consumer CSS on --ds-box-radius cannot round a none box either.
radiusradius.sm2px2pxnoradius: none resolves radius.none and is written out, rather than leaving the property unset — every binding is applied explicitly, with no cascade. Unlike padding, radius is presence-gated: none means no rounded corners, so overrides.radius is ignored at none and applies at every other value. As with surface: none, the none rule writes radius.none directly and reads no hook, so consumer CSS on --ds-box-radius cannot round a none box either.
radiusradius.md4px4pxnoradius: none resolves radius.none and is written out, rather than leaving the property unset — every binding is applied explicitly, with no cascade. Unlike padding, radius is presence-gated: none means no rounded corners, so overrides.radius is ignored at none and applies at every other value. As with surface: none, the none rule writes radius.none directly and reads no hook, so consumer CSS on --ds-box-radius cannot round a none box either.
radiusradius.lg6px6pxnoradius: none resolves radius.none and is written out, rather than leaving the property unset — every binding is applied explicitly, with no cascade. Unlike padding, radius is presence-gated: none means no rounded corners, so overrides.radius is ignored at none and applies at every other value. As with surface: none, the none rule writes radius.none directly and reads no hook, so consumer CSS on --ds-box-radius cannot round a none box either.
radiusradius.full999px999pxnoradius: none resolves radius.none and is written out, rather than leaving the property unset — every binding is applied explicitly, with no cascade. Unlike padding, radius is presence-gated: none means no rounded corners, so overrides.radius is ignored at none and applies at every other value. As with surface: none, the none rule writes radius.none directly and reads no hook, so consumer CSS on --ds-box-radius cannot round a none box either.

Component doc

BindingTokenLightDarkLockedDescription
currentColorcolor.foreground #2a2b2f #ecedeeyespart current: The current page, rendered as text with aria-current, in the regular weight.
itemColorcolor.foreground.muted #56585c #bcbec1yespart item: Set on the item (the <li> on web and Lit), so it reaches an ancestor without href rendered as plain text (a level that has no page of its own); the Link and the current page set their own colours over it. On native, where colour does not inherit through a View, the plain ancestor Text takes it directly.
separatorColorcolor.foreground.muted #56585c #bcbec1yespart separator: A slash or chevron between items, aria-hidden. On web and Lit the separator is the ::before of every item after the first, so it has no element, data-part, minTarget or focus hook of its own and this binding styles that pseudo-element; on native it is a Text with testID Breadcrumb.separator.
gapspace.28px8pxnopart list: Applied twice, never as margins: as the list’s column gap between items, and as the gap inside each item between its leading separator and its content. The separator belongs to the item after it, so a wrapped line may start with a separator. The ellipsis sits in its own item like any other, so it carries a leading separator too and a collapsed trail reads Docs / … / Breadcrumb / Keyboard. Row gap between wrapped lines is none; the items’ minTarget height spaces the lines. An override of gap applies in both places.
focusRingcolor.border.focus #496ded #99b5ffyespart item: Only the focus-fallback item (the <li> given tabindex="-1" after expanding, web and Lit) draws it, as an outline under :focus-visible; Link and Button draw their own rings. Not applied on native, where that focus is screen-reader focus only. Locked bindings still get their —ds-breadcrumb-* hook on both web and Lit — locked means absent from the overrides type, not absent from CSS — so this ring stays reachable from consumer CSS. Native :focus-visible semantics are kept as they are: after a mouse press on the ellipsis the browser does not match the programmatically focused <li>, so no ring is drawn, and no :focus fallback is added to force one.
focusRingWidthborder.width.focus2px2pxyespart item: Outline width of the focus-fallback item ring; web and Lit only.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart nav: Set on the nav and inherited on web and Lit; on native forwarded like fontSize.
fontSizefont.size.sm13px13pxnopart nav: Set on the nav; the Links inherit it on web and Lit. On native each ancestor Link is wrapped in a Text (inside the link part’s View) whose overrides receive fontSize, fontFamily, fontWeight and lineHeight (and any override of them), and plain ancestors, the current page and separators are Text at the same values, so the type is one value everywhere.
fontWeightfont.weight.regular400400nopart nav: Set on the nav and inherited on web and Lit; on native forwarded like fontSize.
lineHeightfont.lineHeight.normal1.51.5nopart nav: Set on the nav and inherited on web and Lit; on native forwarded like fontSize.
minTargetsize.target.min24px24pxyespart item: Each item reaches 24px tall via min-height on the list item, not on the inline Link.

Button

Component doc

BindingTokenLightDarkLockedDescription
backgroundcolor.action.primary.background #3553d2 #3553d2yes
backgroundcolor.action.secondary.background #ecedee #414246yes
backgroundcolor.action.ghost.background transparent transparentyes
backgroundcolor.action.danger.background #b51d26 #d33a3cyes
backgroundHovercolor.action.primary.backgroundHover #243ab3 #496dedyesstate hover: Pointer hover and pressed state (:hover and :active on web and Lit; pressed on rn, which does not read the Pressable hovered state, so a react-native-web mouse hover leaves the fill alone). The declared state: hover names the token slot; the rule applies the value to :is(:hover, :active), as the two read as one pressed-or-pointed state here. Never applied while disabled or loading, since neither accepts a press; the same holds for inverseBackgroundHover. Web and Lit suppress it by selector — :not([aria-disabled='true']):not([aria-busy='true']) — so an aria-busy a caller passes through ...rest also suppresses the fill, which is right: a busy button is not accepting presses either. Locked like background and foreground, so an override cannot put an unproven fill behind the locked foreground.
backgroundHovercolor.action.secondary.backgroundHover #d9dbdd #56585cyesstate hover: Pointer hover and pressed state (:hover and :active on web and Lit; pressed on rn, which does not read the Pressable hovered state, so a react-native-web mouse hover leaves the fill alone). The declared state: hover names the token slot; the rule applies the value to :is(:hover, :active), as the two read as one pressed-or-pointed state here. Never applied while disabled or loading, since neither accepts a press; the same holds for inverseBackgroundHover. Web and Lit suppress it by selector — :not([aria-disabled='true']):not([aria-busy='true']) — so an aria-busy a caller passes through ...rest also suppresses the fill, which is right: a busy button is not accepting presses either. Locked like background and foreground, so an override cannot put an unproven fill behind the locked foreground.
backgroundHovercolor.action.ghost.backgroundHover #f6f7f8 #2a2b2fyesstate hover: Pointer hover and pressed state (:hover and :active on web and Lit; pressed on rn, which does not read the Pressable hovered state, so a react-native-web mouse hover leaves the fill alone). The declared state: hover names the token slot; the rule applies the value to :is(:hover, :active), as the two read as one pressed-or-pointed state here. Never applied while disabled or loading, since neither accepts a press; the same holds for inverseBackgroundHover. Web and Lit suppress it by selector — :not([aria-disabled='true']):not([aria-busy='true']) — so an aria-busy a caller passes through ...rest also suppresses the fill, which is right: a busy button is not accepting presses either. Locked like background and foreground, so an override cannot put an unproven fill behind the locked foreground.
backgroundHovercolor.action.danger.backgroundHover #940014 #b51d26yesstate hover: Pointer hover and pressed state (:hover and :active on web and Lit; pressed on rn, which does not read the Pressable hovered state, so a react-native-web mouse hover leaves the fill alone). The declared state: hover names the token slot; the rule applies the value to :is(:hover, :active), as the two read as one pressed-or-pointed state here. Never applied while disabled or loading, since neither accepts a press; the same holds for inverseBackgroundHover. Web and Lit suppress it by selector — :not([aria-disabled='true']):not([aria-busy='true']) — so an aria-busy a caller passes through ...rest also suppresses the fill, which is right: a busy button is not accepting presses either. Locked like background and foreground, so an override cannot put an unproven fill behind the locked foreground.
foregroundcolor.action.primary.foreground #ffffff #ffffffyes
foregroundcolor.action.secondary.foreground #000000 #ffffffyes
foregroundcolor.action.ghost.foreground #3553d2 #99b5ffyes
foregroundcolor.action.danger.foreground #ffffff #ffffffyes
iconGapspace.28px8pxnoGap between an icon and the label. Deliberately a step of the numeric scale rather than the t-shirt ramp the paddings use: the gap between a glyph and its word is one fixed distance and does not grow with size.
paddingInlinespace.sm8px8pxno
paddingInlinespace.md12px12pxno
paddingInlinespace.lg16px16pxno
paddingBlockspace.sm8px8pxno
radiusradius.md4px4pxno
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifno
fontWeightfont.weight.medium500500no
fontSizefont.size.sm13px13pxno
fontSizefont.size.md16px16pxno
fontSizefont.size.lg19px19pxno
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyes
focusRingOffsetborder.width.focus2px2pxyesOutline offset, so the ring clears the button’s own fill instead of sitting on its edge. The same focus token as focusRingWidth and locked with it, so the rule reads the token directly and there is no literal.
inverseForegroundcolor.inverse.link #99b5ff #243ab3yesghost text when inverse.
inverseFocusRingcolor.inverse.focus #99b5ff #243ab3yesFocus ring when inverse.
inverseBackgroundHovercolor.inverse.foreground #f6f7f8 #17181bnostate hover: ghost hover and pressed fill when inverse: this color at inverseHoverOpacity. The color-mix on web and Lit takes transparent as its second colour, never color.inverse.surface — ghost has no fill of its own, so whatever the Toast or panel actually paints has to show through, and mixing against the token would be right only when the two happen to match. On native it is the same thing by another route: the resolved colour with an alpha channel, drawn over whatever the transparent ghost sits on; a colour that is not #rgb or #rrggbb passes through without alpha.
inverseHoverOpacityopacity.disabled0.50.5nostate hover: The alpha of inverseBackgroundHover: color-mix percentage on web/Lit (calc(var(--ds-button-inverse-hover-opacity) * 0.25 * 100%)), the alpha of the resolved color on native. The ×0.25 applies to whatever token the binding resolves to, an override included: the hook holds the base token and the rule that reads it multiplies.
minTargetsize.target.min24px24pxyesmin-width and min-height of the button on every platform. Web and Lit apply only this floor; there is no coarse-pointer rule.
touchTargetsize.target.comfortable44px44pxyesThe hit area on touch platforms, reached without changing the visual size: hitSlop on rn, .contentShape on SwiftUI. On rn the slop per side is half the shortfall — ceil((touchTarget − measured extent) / 2) — and the footprint is presumed to be minTarget until the first onLayout, so the comfortable target is never under-served on the first frame. react-native-web ignores hitSlop, so in the web preview the target is the visual box alone: it clears the 24px floor through minTarget, and the 44px target is not observable there.
disabledOpacityopacity.disabled0.50.5noApplied to the whole button when disabled; colors are unchanged so contrast math still holds for the enabled state.
transitionmotion.duration.fast120ms120msnoBackground transitions on hover and press, with motion.easing.standard. No variant changes its foreground between states, so only the background animates: the property list is background-color alone, and a variant swapped at runtime therefore changes fill without a transition. Removed under @media (prefers-reduced-motion: reduce) { transition: none } — the reduce query, never an inverted no-preference gate, which would also strip motion where the feature is unsupported.
loadingSpinmotion.duration.loop800ms800msnoOne rotation of the loading indicator, at linear easing (a continuous spin; motion.easing.standard is for transition only); disabled under prefers-reduced-motion, written as the same reduce query as transition. The ring stays rendered and frozen at its start angle rather than being hidden — it is the only visual sign that the button is busy — and transition likewise snaps the background instead of animating.
spinnerSizefont.size.sm13px13pxnoDiameter of the loading ring: 1em of the label font. On rn, which has no em, the ring is this size and its radius is half of it. Its own binding: a fontSize override does not move it, and a consumer who changes one overrides both.
spinnerSizefont.size.md16px16pxnoDiameter of the loading ring: 1em of the label font. On rn, which has no em, the ring is this size and its radius is half of it. Its own binding: a fontSize override does not move it, and a consumer who changes one overrides both.
spinnerSizefont.size.lg19px19pxnoDiameter of the loading ring: 1em of the label font. On rn, which has no em, the ring is this size and its radius is half of it. Its own binding: a fontSize override does not move it, and a consumer who changes one overrides both.
spinnerStrokeborder.width.focus2px2pxyesRing thickness of the loading spinner: a spinnerSize circle with one quarter transparent — the block-start quarter on every platform, so the turn reads as starting from twelve o’clock — drawn in currentColor (the resolved foreground binding on rn). Locked because border.width.focus is a focus token: it keeps its --ds-button-spinner-stroke hook but is not a member of the overrides type. That the hook stays settable from document CSS is intended — locked removes a binding from overrides, never from the CSS escape hatch. The ring is round through radius.full, read directly with no hook, on web and Lit, and through a radius of spinnerSize / 2 on rn, where it is a bordered View with a transparent top border (no react-native-svg) and carries no testID. One rotation is a full turn, rotate(360deg): a geometric constant, not a themed value.

Card

Component doc

BindingTokenLightDarkLockedDescription
paddingBlocklayout.inset.sm8px8pxno
paddingBlocklayout.inset.md12px12pxno
paddingBlocklayout.inset.lg16px16pxno
paddingInlinelayout.inset.sm8px8pxno
paddingInlinelayout.inset.md12px12pxno
paddingInlinelayout.inset.lg16px16pxno
partGaplayout.gap.loose16px16pxnoVertical gap between header, body and footer.
headerGaplayout.gap.normal8px8pxnopart header: Horizontal gap between the heading and headerActions.
footerGaplayout.gap.tight4px4pxnopart footer: Horizontal gap between footer actions.
actionsGaplayout.gap.tight4px4pxnopart headerActions: Horizontal gap between the headerActions controls.
backgroundcolor.background #ffffff #17181byes
backgroundcolor.background.subtle #f6f7f8 #2a2b2fyes
bordercolor.border #d9dbdd #414246noIn effect with surface default only; a subtle card draws no border, so an override of this binding changes nothing there.
borderWidthborder.width.thin1px1pxnoRendered only with surface default. An interactive card always reserves border.width.focus instead, so the ring appearing never shifts the layout; at rest that border is colored border on surface default (the card keeps its visible border) and transparent on subtle, and focusRing while the ring shows. The reserve is keyed on the interactive prop alone, not on whether a target was found, so a card that fell back to non-interactive still reserves the width rather than changing its geometry on a content change. React Native reserves it for focusable cards too, since it has no outline to draw outside the box. An override of this binding therefore only changes cards that are neither interactive nor (on native) focusable.
radiusradius.lg6px6pxno
hoverBackgroundcolor.background.subtle #f6f7f8 #2a2b2fyesstate hover: Interactive cards only, on pointer hover (a plain :hover on web and Lit, no @media (hover: hover) guard, as Link). The surface picks the token — a subtle card cannot hover to its own background — and the same lookup applies on every platform. Native has no hover, so the Pressable shows it while pressed, and on pointer hover where the platform reports one (iPad pointer, react-native-web).
hoverBackground (surface=default)color.background.subtle #f6f7f8 #2a2b2fyesstate hover: Interactive cards only, on pointer hover (a plain :hover on web and Lit, no @media (hover: hover) guard, as Link). The surface picks the token — a subtle card cannot hover to its own background — and the same lookup applies on every platform. Native has no hover, so the Pressable shows it while pressed, and on pointer hover where the platform reports one (iPad pointer, react-native-web).
hoverBackground (surface=subtle)color.background.strong #ecedee #414246yesstate hover: Interactive cards only, on pointer hover (a plain :hover on web and Lit, no @media (hover: hover) guard, as Link). The surface picks the token — a subtle card cannot hover to its own background — and the same lookup applies on every platform. Native has no hover, so the Pressable shows it while pressed, and on pointer hover where the platform reports one (iPad pointer, react-native-web).
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyes
transitionmotion.duration.fast120ms120msnoInteractive hover, with motion.easing.standard. Native has no continuous hover to animate between — the pressed style swaps instantly — so the binding exists there for API parity and has no runtime effect.

Component doc

BindingTokenLightDarkLockedDescription
slideGaplayout.gap.normal8px8pxnopart slide: Applied as the track’s gap, never a slide margin (on Lit the slides are slotted, so only the track can carry it).
controlOffsetspace.28px8pxnoDistance of the arrow buttons from the viewport edge when overlaid: the inline inset of each controlSurface from the viewport’s inline edge.
controlRadiusradius.full999px999pxnoThe controlSurface wrapper’s corner radius.
controlBackgroundcolor.overlay.surface #ffffff #2a2b2fyesThe controlSurface wrapper around each arrow Button (the Button itself is secondary and untouched) so they stay readable over images.
controlShadowshadow.raised0px 1px 3px 0px #0000001a0px 1px 3px 0px #00000059noOn the controlSurface wrapper.
pickerGaplayout.gap.tight4px4pxnopart picker.
pickerOffsetspace.312px12pxnopart picker: Between the viewport and the picker row below it, and between the play/pause row above it and the viewport; a gap between rows, not a margin.
dotcolor.border.strong #939599 #707176yes
dotActivecolor.control.selectedBackground #3553d2 #496dedyes
dotSizespace.28px8pxno
dotTargetsize.target.min24px24pxyesEach dot’s hit area; the dot itself is small.
dotRadiusradius.full999px999pxnopart pickerItem: Dots are round; a tab has no radius. The part is the picker item, but the value reaches both the dot mark inside it and that item’s focus ring, so one override reshapes the two together. A tab keeps a square ring whatever this says, so an override has no effect in a tabs picker.
radiusradius.md4px4pxnoApplied to the viewport so slide edges match the theme.
tabColorcolor.foreground.muted #56585c #bcbec1yespart pickerItem: Text of an unselected tab in a tabs picker.
tabSelectedColorcolor.foreground.strong #000000 #ffffffyespart pickerItem.
tabFontSizefont.size.sm13px13pxnopart pickerItem.
tabFontWeightfont.weight.medium500500nopart pickerItem: The same weight selected or not, so selection never shifts the row.
tabLineHeightfont.lineHeight.normal1.51.5nopart pickerItem: The tab label’s line height, so a tabs picker keeps the row’s rhythm. It is a multiplier, so React Native, which needs an absolute value, resolves it against the effective tabFontSize: overriding the font size rescales the line height with it.
tabPaddingBlockspace.sm8px8pxnopart pickerItem.
tabPaddingInlinespace.md12px12pxnopart pickerItem.
tabIndicatorThicknessborder.width.focus2px2pxyespart pickerItem: The selected tab’s underline, drawn in dotActive inside the tab’s box (an inset, so it takes no layout).
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart pickerItem.
transitionmotion.duration.base200ms200msnoDot background and tab text colour changes, with motion.easing.standard (weights never change, so nothing else animates); slide movement is the browser’s scroll-snap timing (instant under reduced motion).
minTargetsize.target.comfortable44px44pxyesThe hit area of each arrow’s controlSurface — the part wrapper inside it stretches to fill that surface, while the composed Button keeps its own intrinsic size and is centred in it, since a child is never restyled; on web and Lit the extra area is the wrapper’s click-through, and on React Native, where the wrapper is inert, the pressable area is the Button’s alone — and the minimum block size of each tab, whose inline size comes from tabPaddingInline alone; dots use dotTarget.
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyes

Checkbox

Component doc

BindingTokenLightDarkLockedDescription
controlBackgroundcolor.control.background #ffffff #2a2b2fnopart control.
controlBordercolor.control.border #939599 #707176yespart control.
controlBorderWidthborder.width.thin1px1pxnopart control.
controlSelectedBackgroundcolor.control.selectedBackground #3553d2 #496dedyespart control: Checked and indeterminate fill; the border takes the same color unless the box is invalid (see controlBorderInvalid).
indicatorcolor.control.selectedForeground #ffffff #000000yespart indicator: The check mark (Icon name="check", when checked) and the mixed dash (Icon name="dash", when the mixed indicator shows) in this color, at size: xs, centered in the control; nothing is rendered when unchecked. The color reaches the Icon only through its color override (a token path); the Checkbox hook does not style the Icon. The web/Lit control is a void , so the control and the indicator sit in a box wrapper span that stacks them in one cell: the input keeps data-part="control", and the indicator is an aria-hidden span with pointer-events: none over the input holding the Icon, carrying data-part="indicator" (web) / part="indicator" and data-part="indicator" (Lit). On native the Icon sits inside the drawn control View.
indicatorStrokeborder.width.focus2px2pxyespart indicator: Stroke thickness of the check mark and dash. Locked, as Icon’s own strokeWidth is and on the same token, so the two already agree and nothing is forwarded into the composed Icon. It declares no —ds-checkbox-* hook on any platform: Icon already applies it.
pressedOverlayopacity.disabled0.50.5nopart control, state pressed: While pressed (web/Lit :active), an unchecked, not-mixed, enabled box shows controlSelectedBackground at this opacity over controlBackground; the border is unchanged. A checked or mixed box (already filled) and a disabled one show no overlay. Web/Lit: color-mix(in srgb, <controlSelectedBackground> calc(<pressedOverlay> * 100%), <controlBackground>) as the control background. Native: an absolutely filled overlay View inside the control, so the border does not fade. Sharing opacity.disabled with disabledOpacity is deliberate, not a placeholder: the theme has one “recede” opacity and both states use it, so a brand that retunes it moves the dim and the press tint together.
controlBorderInvalidcolor.border.danger #b51d26 #ff958dnopart control: Border color while invalid (invalid or error), in every state: it replaces controlBorder on an unchecked box and the controlSelectedBackground-colored border on a checked or mixed box (the fill stays controlSelectedBackground). Border color precedence: invalid, then selected, then rest. Focus never hides it: on web/Lit the focus ring is a separate outline; on native, where focus is drawn on the border, a focused invalid box keeps this color and only the width changes to focusRingWidth, as in Input.
controlSizespace.520px20pxnopart control.
controlRadiusradius.sm2px2pxnopart control.
gapspace.28px8pxnopart label: Horizontal gap between control and label (the row’s flex gap). The gap is part of the hit area: a press on it toggles.
partGapspace.14px4pxnopart description: Vertical gap between label and description (the text column inside the row), and between the row and the error message below it (the root column’s gap). The error message is indented by controlSize + gap on every platform, as padding-inline-start on the message itself, never a margin, so it lines up with the label rather than the control. The indent is unconditional, hideLabel included, so the message keeps the same inset whether or not a label is visible.
labelColorcolor.foreground #2a2b2f #ecedeeyespart label: Web/Lit: the native
labelSizefont.size.md16px16pxnopart label: Web/Lit: the native
labelWeightfont.weight.regular400400nopart label: Web/Lit: the native
helperSizefont.size.sm13px13pxnopart description: Description and error text size. Reaches the composed Texts only through their fontSize override, with fontFamily and lineHeight forwarded the same way; no —ds-checkbox-* hook, the documented exception to the rule that every binding gets one, because there is no element of Checkbox’s own to hang it on. fontFamily and lineHeight do keep their hooks, since they are also the label’s own rule.
descriptionTextcolor.foreground.muted #56585c #bcbec1yespart description: Realised by the composed Text’s muted tone; no —ds-checkbox-* hook.
errorTextcolor.foreground.danger #b51d26 #ff958dyespart errorMessage: Realised by the composed Text’s danger tone; no —ds-checkbox-* hook.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart label: The label’s own rule on web/Lit (forwarded to the label Text on native), and forwarded to the description and error Texts.
lineHeightfont.lineHeight.normal1.51.5nopart label: As fontFamily: the label’s own rule on web/Lit, forwarded to every composed Text.
focusRingcolor.border.focus #496ded #99b5ffyespart control: Web/Lit: an outline of focusRingWidth in this color around the control on :focus-visible. Native (hardware keyboard focus): the control’s border takes this color at focusRingWidth while focused (an invalid box keeps controlBorderInvalid and only the width changes); the box keeps its size, so the glyph area shrinks by the width difference. The focused width is never thinner than the rest width: it is the larger of focusRingWidth and controlBorderWidth, so a controlBorderWidth override wider than border.width.focus keeps its width on focus.
focusRingWidthborder.width.focus2px2pxyespart control: Width of the focus ring, and on web and Lit its outline-offset as well, so the ring clears the control’s own border rather than sitting on it.
minTargetsize.target.comfortable44px44pxyespart control: Minimum height of the control + label row. It is the comfortable target, above the 24px floor target-24px asks for, so satisfying this binding satisfies the requirement everywhere. the whole row is the hit area, including the gap. The row aligns its children to the start of the cross axis, with paddingBlock max(0, (minTarget − labelSize × lineHeight) / 2) on each side — clamped at zero, so an override that makes the line box taller than the target grows the row instead of producing a negative padding — and minTarget as a minimum height, so a single-line row is exactly this tall. The control is not offset by a margin: its box is one label line box tall (labelSize × lineHeight) with the control centred inside it, which is how it lands on the label’s first line, and a wrapping label or a description grows the row downwards without pulling it off that line (as Switch). The construction is the same with hideLabel — the hidden label still sets the line box, so the control stays centred and the row keeps its height — which is also why hideLabel together with a description puts the control on the empty label line, above the description’s first line rather than beside it. The error message sits below the row, outside the hit area.
disabledOpacityopacity.disabled0.50.5nopart control: Dims the control (with its indicator) and the label. The description and error stay at full opacity so they remain readable. The dimmed label does not meet AA on its own, and contrast-aa and this binding coexist only because a disabled control is exempt: the control must therefore always report its disabled state to the platform (aria-disabled on web and Lit, and mirrored onto the DOM node under react-native-web), or the dim becomes a real contrast failure.
transitionmotion.duration.fast120ms120msnopart control: The control’s background (fill) and border color transitions in every state, with motion.easing.standard — the invalid and focus border colors are part of it, so entering and leaving those states cross-fades like a check does rather than snapping. The indicator is not animated: the check and dash Icons are mounted and unmounted (nothing is rendered when unchecked), so they appear, disappear and swap check↔dash instantly. Instant under reduced motion.

Combobox

Component doc

BindingTokenLightDarkLockedDescription
fieldBackgroundcolor.background #ffffff #17181byespart field.
fieldBordercolor.border.strong #939599 #707176yespart field.
fieldBorderFocuscolor.border.focus #496ded #99b5ffyespart field: While the input itself has focus (web and Lit: :has(input:focus-visible), not :focus-within, so a focused clear or chip-remove Button shows only its own ring).
fieldBorderInvalidcolor.border.danger #b51d26 #ff958dnopart field.
fieldBorderWidthborder.width.thin1px1pxnopart field.
fieldRadiusradius.md4px4pxnopart field.
fieldPaddingInlinespace.md12px12pxnopart field.
fieldPaddingBlockspace.sm8px8pxnopart field.
fieldGaplayout.gap.tight4px4pxnopart field: Between chips, input text and the buttons.
inputColorcolor.foreground #2a2b2f #ecedeeyespart input.
placeholderColorcolor.foreground.muted #56585c #bcbec1yespart input: The input placeholder (::placeholder; placeholderTextColor on native).
chipBackgroundcolor.background.strong #ecedee #414246yespart chip.
chipColorcolor.foreground #2a2b2f #ecedeeyespart chip.
chipRadiusradius.full999px999pxnopart chip.
chipPaddingInlinespace.28px8pxnopart chip.
chipPaddingBlockspace.00px0pxnopart chip.
chipGaplayout.gap.tight4px4pxnopart chip: Between chip label and its remove button.
iconColorcolor.foreground.muted #56585c #bcbec1yesToggle chevron, clear and chip-remove icons; forwarded to each composed Icon’s own color override as this token, on every platform — React Native included, where it goes to the Icon’s overrides.color rather than its color prop, so the combobox never resolves a token on the Icon’s behalf. Locked, it keeps its --ds-combobox-icon-color hook on web and Lit, as every locked binding does; the hook reaches each Icon by setting that Icon’s own documented --ds-icon-color hook on its host, never its internals.
partGapspace.14px4pxnoBetween label, description, field and error message.
labelWeightfont.weight.medium500500nopart label: Forwarded to the label Text’s own fontWeight override.
helperSizefont.size.sm13px13pxnoForwarded to the description and error Text’s own fontSize override.
descriptionTextcolor.foreground.muted #56585c #bcbec1yespart description: Also the colour of the status part where it is visible (React Native), which takes the description treatment — this token and helperSize — and has no binding of its own, so an override reaches it only through these two.
errorTextcolor.foreground.danger #b51d26 #ff958dyes
popupSurfacecolor.overlay.surface #ffffff #2a2b2fnopart popup.
popupBordercolor.border #d9dbdd #414246nopart popup.
popupBorderWidthborder.width.thin1px1pxnopart popup.
popupShadowshadow.overlay0px 8px 24px 0px #0000002e0px 8px 24px 0px #00000080nopart popup.
popupRadiusradius.md4px4pxnopart popup.
popupOffsetspace.14px4pxnopart popup: The gap between field and popup. Applied as the fixed-position popup’s block margin (it has no parent gap to use), as Select; only the side facing the field shows.
layerlayer.dropdown100100no
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnoLabel and input text.
fontSizefont.size.md16px16pxnoLabel and input text.
chipSizefont.size.sm13px13pxnopart chip.
lineHeightfont.lineHeight.normal1.51.5noLabel and input text.
minTargetsize.target.comfortable44px44pxyespart field: The field as a whole.
inputMinTargetsize.target.min24px24pxyespart input: The text input inside the field, which needs its own floor: with multiple the field grows to several chip rows and the input is only one of them.
focusRingWidthborder.width.focus2px2pxyespart field: Replaces fieldBorderWidth while the input is focused (the field border is its focus ring, as Input); fieldPaddingInline and fieldPaddingBlock shrink by the difference so the content does not shift.
disabledOpacityopacity.disabled0.50.5no
entermotion.duration.fast120ms120msnoPopup opacity fade-in and the field border-color transition; instant under reduced motion.

Container

Component doc

BindingTokenLightDarkLockedDescription
maxWidthlayout.maxWidth.prose572px572pxnofull renders no max-width — the literal none, with no hook, which also makes an override of this binding a no-op at that value; the binding covers the other three. No dev warning fires for an override that has no effect.
maxWidthlayout.maxWidth.content960px960pxnofull renders no max-width — the literal none, with no hook, which also makes an override of this binding a no-op at that value; the binding covers the other three. No dev warning fires for an override that has no effect.
maxWidthlayout.maxWidth.page1280px1280pxnofull renders no max-width — the literal none, with no hook, which also makes an override of this binding a no-op at that value; the binding covers the other three. No dev warning fires for an override that has no effect.
maxWidthlayout.maxWidth.full——nofull renders no max-width — the literal none, with no hook, which also makes an override of this binding a no-op at that value; the binding covers the other three. No dev warning fires for an override that has no effect.
paddingInlinelayout.gutter.narrow16px16pxnonone renders no padding (a literal 0, with no hook), which also makes an override of this binding a no-op at that value, with no dev warning. narrow, wide and none are fixed at every viewport: their selectors carry the attribute or modifier class, so they outrank the bare responsive rules inside both media queries and no viewport can move them. Only default is responsive: layout.gutter.narrow below layout.maxWidth.content, layout.gutter.default (the --layout-gutter variable) from layout.maxWidth.content, and layout.gutter.wide from layout.maxWidth.page. Both boundaries are inclusive (>=, a min-width query), so a viewport exactly at a token width takes the wider gutter. An override of this binding replaces the value at every viewport width, including the whole responsive default gutter, not just its middle band.
paddingInlinelayout.gutter24px24pxnonone renders no padding (a literal 0, with no hook), which also makes an override of this binding a no-op at that value, with no dev warning. narrow, wide and none are fixed at every viewport: their selectors carry the attribute or modifier class, so they outrank the bare responsive rules inside both media queries and no viewport can move them. Only default is responsive: layout.gutter.narrow below layout.maxWidth.content, layout.gutter.default (the --layout-gutter variable) from layout.maxWidth.content, and layout.gutter.wide from layout.maxWidth.page. Both boundaries are inclusive (>=, a min-width query), so a viewport exactly at a token width takes the wider gutter. An override of this binding replaces the value at every viewport width, including the whole responsive default gutter, not just its middle band.
paddingInlinelayout.gutter.wide32px32pxnonone renders no padding (a literal 0, with no hook), which also makes an override of this binding a no-op at that value, with no dev warning. narrow, wide and none are fixed at every viewport: their selectors carry the attribute or modifier class, so they outrank the bare responsive rules inside both media queries and no viewport can move them. Only default is responsive: layout.gutter.narrow below layout.maxWidth.content, layout.gutter.default (the --layout-gutter variable) from layout.maxWidth.content, and layout.gutter.wide from layout.maxWidth.page. Both boundaries are inclusive (>=, a min-width query), so a viewport exactly at a token width takes the wider gutter. An override of this binding replaces the value at every viewport width, including the whole responsive default gutter, not just its middle band.
paddingInlinelayout.gutter.none——nonone renders no padding (a literal 0, with no hook), which also makes an override of this binding a no-op at that value, with no dev warning. narrow, wide and none are fixed at every viewport: their selectors carry the attribute or modifier class, so they outrank the bare responsive rules inside both media queries and no viewport can move them. Only default is responsive: layout.gutter.narrow below layout.maxWidth.content, layout.gutter.default (the --layout-gutter variable) from layout.maxWidth.content, and layout.gutter.wide from layout.maxWidth.page. Both boundaries are inclusive (>=, a min-width query), so a viewport exactly at a token width takes the wider gutter. An override of this binding replaces the value at every viewport width, including the whole responsive default gutter, not just its middle band.

DataGrid

Component doc

BindingTokenLightDarkLockedDescription
surfacecolor.background #ffffff #17181byespart container.
headerSurfacecolor.background.subtle #f6f7f8 #2a2b2fyespart header.
headerColorcolor.foreground #2a2b2f #ecedeeyespart header.
headerWeightfont.weight.semibold600600nopart header.
headerSizefont.size.sm13px13pxnopart header.
headerBordercolor.border.strong #939599 #707176nopart header.
headerBorderWidthborder.width.thin1px1pxnopart header.
headerShadowshadow.raised0px 1px 3px 0px #0000001a0px 1px 3px 0px #00000059nopart header: Under the sticky header once the body has scrolled.
gridLinecolor.border #d9dbdd #414246nopart grid: Cell borders on both axes — a grid shows its cells, unlike Table which shows rows.
gridLineWidthborder.width.thin1px1pxnopart grid.
rowHeightsize.target.min24px24pxyespart row: Compact rows are the minimum target height (rows are the unit people click); comfortable rows use rowHeightComfortable. Virtualization measures one rendered row (ResizeObserver) rather than reading the token, so density and overrides both work. Until a row has been measured (and always in jsdom) rows are positioned with calc(index × the row size) from the token and at most 50 render (literal-ok). A grid with a selection column is rowHeightComfortable at both densities, on every platform: the select Checkbox is a full minTarget and a compact row — the same minimum, less its grid lines — cannot hold one, so selectable: row and density: compact together give comfortable rows rather than a clipped or overlapping target.
rowHeightComfortablesize.target.comfortable44px44pxyespart row.
rowHovercolor.action.ghost.backgroundHover #f6f7f8 #2a2b2fnopart row, state hover: Row under the pointer, the select cell included; the grid is interactive by nature so hover is allowed, and the focused cell ring is the non-hover signal. On native it is the pressed fill (and the react-native-web hover); a selected row keeps rowSelected.
rowSelectedcolor.background.subtle #f6f7f8 #2a2b2fyespart row.
rowSelectedBordercolor.control.selectedBackground #3553d2 #496dedyespart row: Start-edge bar on selected rows.
rowSelectedBorderWidthborder.width.focus2px2pxyespart row.
cellColorcolor.foreground #2a2b2f #ecedeeyespart cell.
cellMutedColorcolor.foreground.muted #56585c #bcbec1yespart cell: Text of the existing body cells while loading (they stay but read as stale).
cellPaddingInlinespace.28px8pxnopart cell.
selectColumnWidthsize.target.min24px24pxyespart selectCell: The selection column’s total inline size is this plus 2 × cellPaddingInline, and the cell has no inline padding of its own — the Checkbox is centred in it, and Checkbox has no inset override to take that padding. Pinned-start offsets add the same total.
columnWidthspace.2080px80pxnoWidth of a column that sets no width (160px); an override replaces the base, the × 2 stays in the rule. Where the base is exposed as a CSS hook the doubled value goes in a second, internal variable the consumer cannot set, so the hook keeps the bare token; the density-selected row height (rowHeight or rowHeightComfortable) resolves into an internal variable the same way.
cellFocusRingcolor.border.focus #496ded #99b5ffyespart cell: Drawn inside the cell (inset) so it is never clipped by neighbors or the scroll region. Shown on the active cell whenever the grid has focus, pointer focus included (on web :focus-within on the grid, since a clicked div is not :focus-visible).
cellFocusRingWidthborder.width.focus2px2pxyespart cell.
cellEditingBackgroundcolor.control.background #ffffff #2a2b2fyespart cell.
cellEditingBordercolor.border.focus #496ded #99b5ffyespart cell.
cellInvalidBordercolor.border.danger #b51d26 #ff958dyespart cell.
cellInvalidBackgroundcolor.status.danger.background #fff2f0 #4c0005yespart cell.
cellInvalidForegroundcolor.status.danger.foreground #b51d26 #ffc3bdyespart cell: The validation message in the status bar: a cellInvalidBackground span that re-scopes the foreground to this token (--color-foreground on web and Lit, TextForegroundContext on native) around a default-tone Text, since Text.color is locked (the claimed pair). The invalid cell itself holds the untouched editor and shows only cellInvalidBackground and cellInvalidBorder (an inset ring on native).
rangeBackgroundcolor.background.strong #ecedee #414246yespart rangeOverlay: Selected range fill, drawn beneath the cell content (so the foreground-on-strong pair is exact); opaque pinned cells and a hovered row cover it. The border marks its edge on that same single overlay element, so all three bindings share the rangeOverlay part and the border is covered by those same pinned cells and hovered row — accepted, because splitting the overlay in two would leave rangeBackground on an element with no part. Range selection is web, lit and swiftui only — selectable: range degrades to row on rn, so the rangeOverlay part, these three bindings and copy.selectedRange have no element and no code there.
rangeBordercolor.control.selectedBackground #3553d2 #496dedyespart rangeOverlay.
rangeBorderWidthborder.width.focus2px2pxyespart rangeOverlay.
pinnedShadowshadow.raised0px 1px 3px 0px #0000001a0px 1px 3px 0px #00000059nopart cell: Cast by pinned columns once the body has scrolled sideways: start pins on their end edge, end pins on their start edge. The token is applied unmirrored to both sides — mirroring shadow.raised would mean writing a literal offset — so the two sides cast the same shadow in the token’s own direction.
resizeHandlecolor.border.strong #939599 #707176nopart resizeHandle: The column edge grab area (role=separator, not focusable; keyboard resizing is Shift+Arrow on the header cell), visible on hover/focus of the header cell. Its aria-valuenow is set once the column has a pixel width — an explicit width or one the user has resized — and omitted while the column sits at the columnWidth token, whose pixel value the grid does not know at render. Its aria-valuemin is the column’s minWidth; a column has no maximum, so aria-valuemax is omitted. Native has no hover, so it is always visible there, hit-slopped to size.target.min.
resizeHandleWidthspace.14px4pxnopart resizeHandle.
resizeStepspace.416px16pxnoWidth change per Shift+Arrow press on a resizable header cell. Pointer and keyboard resizing both need this and minTarget as pixel numbers; a platform that cannot read a token’s computed length measures it from a hidden, zero-height probe element instead of hard-coding one, so an override still moves the step.
statusBarSurfacecolor.background.subtle #f6f7f8 #2a2b2fyespart statusBar: The statusBar part stays on the live status Text (the composed Text that carries statusBarSize); statusBarSurface, statusBarPadding and statusBarGap style the bar element the grid owns around that Text and its sibling Texts, which carries no part of its own.
statusBarColorcolor.foreground.muted #56585c #bcbec1yespart statusBar: The status bar Texts use tone="muted"; Text.color is locked, so this is not forwarded.
statusBarSizefont.size.xs11px11pxnopart statusBar.
statusBarPaddingspace.28px8pxnopart statusBar.
statusBarGapspace.28px8pxnopart statusBar: Between status bar items, which are laid out in this order and no other: the live message, the row count, the selection count, the scroll hint, the position. No separator characters. The row count is always shown; the selection count only while something is selected; the scroll hint only while the columns overflow the region and it has not yet been scrolled sideways; the position only while the active cell is a body cell — the first header cell is active on entry and has no row number to interpolate, so nothing is shown until focus reaches the body. On rn a cell becomes active only in cell mode (a tap makes it active there); in row and none no cell is ever active, no body cell is pressable, and the position is absent.
captionSizefont.size.md16px16pxnopart caption.
captionWeightfont.weight.semibold600600nopart caption.
captionGapspace.28px8pxnopart caption: Below the caption; the Heading’s own margin is turned off with overrides={{ marginBlockEnd: 'space.0' }}, as Table.
fixedHeightspace.2080px80pxnoThe height for height: fixed. It sizes the whole component — caption, scroll region and status bar — as viewport does, and the scroll region takes what the caption and status bar leave. Overridable by design (a page decides how tall its grid is); space.20 is the floor, not a recommendation. It is deliberately shorter than the chrome plus one row, so height: fixed is only meaningful with an overrides.fixedHeight: a fixed grid without one renders caption, header and status bar and no visible rows, which is the binding working as specified rather than a default to compensate for.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnoThe component’s font, set on the container and inherited by the scroll region and the cells.
fontSizefont.size.sm13px13pxnoSet with fontFamily on the container.
lineHeightfont.lineHeight.tight1.21.2noSet with fontFamily on the container.
numericFontfont.family.monoui-monospace, SFMono-Regular, Menlo, Consolas, monospaceui-monospace, SFMono-Regular, Menlo, Consolas, monospacenopart cell: Cells whose raw value is a number and that have no render.
minTargetsize.target.min24px24pxyesThe floor for column resize (a column’s minWidth never goes below it) and the size of the Checkbox the selection column composes: forwarded to Checkbox’s controlSize and set on the Checkbox itself, since a declaration on an ancestor loses to the child’s own default. A select cell is therefore a full minimum target, and two select cells in neighbouring rows clear the target-size spacing rule.
focusRingcolor.border.focus #496ded #99b5ffyespart scrollRegion: Drawn by the scroll region while the grid inside it has focus, so the ring is never clipped by the region’s own overflow.
focusRingWidthborder.width.focus2px2pxyespart scrollRegion.
transitionmotion.duration.fast120ms120msnoHover and editor open; navigation and scrolling are instant.

DatePicker

Component doc

BindingTokenLightDarkLockedDescription
backgroundcolor.background #ffffff #17181byes
foregroundcolor.foreground #2a2b2f #ecedeeyes
placeholdercolor.foreground.muted #56585c #bcbec1yes
bordercolor.border.strong #939599 #707176yes
borderFocuscolor.border.focus #496ded #99b5ffyes
borderInvalidcolor.border.danger #b51d26 #ff958dno
borderWidthborder.width.thin1px1pxno
radiusradius.md4px4pxno
paddingInlinespace.md12px12pxnoThe input’s inline padding. An override replaces the value at both sizes.
paddingInline (size=sm)space.28px8pxnoThe input’s inline padding. An override replaces the value at both sizes.
paddingBlockspace.sm8px8pxnoThe input’s block padding. An override replaces the value at both sizes.
paddingBlock (size=sm)space.14px4pxnoThe input’s block padding. An override replaces the value at both sizes.
fontSizefont.size.sm13px13pxnoThe field text and the label (font.size.md at md, font.size.sm at sm); description and error use helperSize.
fontSizefont.size.md16px16pxnoThe field text and the label (font.size.md at md, font.size.sm at sm); description and error use helperSize.
rangeSeparatorColorcolor.foreground.muted #56585c #bcbec1yesThe en dash between start and end inputs. The dash is decoration, not an anatomy part: it is an aria-hidden span (Text on native) with no data-part or testID, so this binding’s own hook is the only way to reach it.
calendarSurfacecolor.overlay.surface #ffffff #2a2b2fyesNot forwarded: Popover’s and BottomSheet’s surface is locked to this same token, so the composed overlay’s own surface realises it and an override has no effect.
calendarInsetlayout.inset.md12px12pxnoForwarded to the composed Popover as overrides.inset (web, lit) and to the BottomSheet as overrides.inset (rn), so the sheet pads at this value rather than its own layout.inset.lg.
calendarGaplayout.gap.normal8px8pxnoBetween header, grid and footer.
headerGaplayout.gap.tight4px4pxnopart header: Between the prev/next Buttons and the month and year Selects in the header row. The header’s order in the document is the Tab order — previous month, month Select, year Select, next month — which is not the order the anatomy list happens to use.
footerGaplayout.gap.tight4px4pxnopart footer: Between the Today and Clear Buttons. The footer row is aligned to the inline end, so it reads as an action row under the grid.
daySizesize.target.comfortable44px44pxyespart day: Every day cell is a comfortable square target.
dayGapspace.00px0pxnopart day: Cells touch, so a range reads as one bar; the selected day’s radius gives it shape.
dayRadiusradius.md4px4pxnopart day.
dayHovercolor.action.ghost.backgroundHover #f6f7f8 #2a2b2fnopart day, state hover: Hover background of a day cell. On React Native hover exists only under react-native-web (Pressable onHoverIn/onHoverOut); the binding is wired there and is dead on a device, as every other hover binding is.
daySelectedBackgroundcolor.control.selectedBackground #3553d2 #496dedyespart day.
daySelectedForegroundcolor.control.selectedForeground #ffffff #000000yespart day.
dayInRangeBackgroundcolor.background.strong #ecedee #414246yespart day.
dayTodayBordercolor.control.selectedBackground #3553d2 #496dedyespart day: The today ring. On a selected or in-range today the ring stays and the selected (or in-range) fill takes the background; on a selected today the ring is the fill’s own colour and so not visible, which is accepted: the selected state wins visually and today is still announced (aria-current, copy.todayLabel). While the day has focus the focus ring replaces it.
dayTodayBorderWidthborder.width.focus2px2pxyespart day.
dayOutsideMonthColorcolor.foreground.muted #56585c #bcbec1yespart day.
weekdayColorcolor.foreground.muted #56585c #bcbec1yespart weekdayHeader.
weekdaySizefont.size.xs11px11pxnopart weekdayHeader: Weekday header cells. Each of the seven header cells carries the weekdayHeader part (so a query for it returns seven, as day and weekNumber do), never the header row.
weekdayWeightfont.weight.medium500500nopart weekdayHeader.
weekNumberSizefont.size.xs11px11pxnopart weekNumber: Week-number cells, in weekdayColor at weekNumberWeight. The week column’s header cell carries no data-part, so it takes the weekdayHeader styling rather than the week-number styling — it is a weekday-row cell that happens to name the week column.
weekNumberWeightfont.weight.regular400400nopart weekNumber: Week-number cells’ weight.
monthTitleSizefont.size.md16px16pxnoForwarded to the month and year Selects as overrides.fontSize.
monthTitleWeightfont.weight.semibold600600noForwarded to the month and year Selects as overrides.fontWeight.
partGapspace.14px4pxnoBetween label, description, field and error.
fieldGapspace.28px8pxnopart field: The field row’s gap, applied uniformly: between the input(s) and the calendar button, and on both sides of a range’s en-dash separator, so the dash sits at the same distance as the button.
dayFontSizefont.size.sm13px13pxnopart day.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifno
lineHeightfont.lineHeight.normal1.51.5noThe field text’s line height. It does not reach the description or errorMessage Texts — only helperSize and fontFamily are forwarded to them, and a Text’s line height is otherwise its own.
labelWeightfont.weight.medium500500nopart label.
helperSizefont.size.sm13px13pxno
descriptionTextcolor.foreground.muted #56585c #bcbec1yespart description.
errorTextcolor.foreground.danger #b51d26 #ff958dyes
minTargetsize.target.comfortable44px44pxyes
minTargetSmsize.target.min24px24pxyesThe field height floor at size sm; the calendar Button becomes size sm. The calendar popup is unchanged.
focusRingcolor.border.focus #496ded #99b5ffyesThe field’s focus-visible ring and the focused day’s ring, which replaces the today ring while that day has focus. The day has no ring binding of its own: it borrows this one at focusRingWidth, drawn with the today ring’s geometry (inset on React Native, where there is no outline) so the two never shift the cell.
focusRingWidthborder.width.focus2px2pxyes
disabledOpacityopacity.disabled0.50.5no
transitionmotion.duration.fast120ms120msnoDay-cell hover and selection states animate background-color, color and (web, lit) the today ring’s border-color; a month change is instant. Instant under reduced motion. On React Native it is background colour only, through one fill layer’s opacity: the fill fades in when it appears and out when it goes, while a fill whose colour merely changes (hover to selected, selected to in-range) switches at once, as the day text colour does — cross-fading two background colours would need a second layer per state. The today ring there is an inset ring, not a border, and switches at once.

Dialog

Component doc

BindingTokenLightDarkLockedDescription
scrimcolor.overlay.scrim #00000066 #00000099nopart scrim.
surfacecolor.overlay.surface #ffffff #2a2b2fyespart surface.
bordercolor.border #d9dbdd #414246noHairline; the only edge in a flat theme.
borderWidthborder.width.thin1px1pxno
shadowshadow.overlay0px 8px 24px 0px #0000002e0px 8px 24px 0px #00000080no
radiusradius.lg6px6pxno
insetlayout.inset.lg16px16pxnoInline padding of header, body and footer, plus the block padding of the surface column that holds them (once at the top, once at the bottom), so nothing doubles between parts. The header and the footer wrapper apply the inline padding themselves; the body Box receives it as overrides.paddingInline and keeps zero block padding. The forward always reaches the Box: on web and Lit through the stylesheet setting the Box’s --ds-box-padding-inline to --ds-dialog-inset — that hook is the whole delivery, since an element-level overrides.paddingInline would only set the same custom property again; on rn the Box takes inset="none" and the token path (the override, else layout.inset.lg) in overrides.paddingInline, a path and never a resolved value, since a parent does not resolve a token for its child.
partGaplayout.gap.loose16px16pxnoGap between header, body and footer, and the only space between them.
gutterlayout.gutter24px24pxnoMinimum space between the surface and the viewport edge: web and Lit cap the surface at 100vw - 2 × gutter wide and 100dvh - 2 × gutter tall — never the , which fills the viewport and holds the scrim, so capping it would pull the scrim off the edges; rn pads the centring container horizontally with it (never a margin) and caps the surface at the window height − 2 × gutter.
headerGaplayout.gap.normal8px8pxnopart header: Between title/description and the close button.
footerGaplayout.gap.tight4px4pxnopart footer: Between footer actions; forwarded to the footer Stack as overrides.gap. On web and Lit the CSS hook reaches the Stack too: the footer wrapper’s stylesheet sets the Stack’s own --ds-stack-gap hook on the Stack element to --ds-dialog-footer-gap, so either route changes the gap. The footer row is end-aligned (Form’s action-row rule), unlike Card’s start-aligned footer.
descriptionGaplayout.gap.tight4px4pxnopart header: Between the heading and the description: the flex gap of the titles group, a Dialog-owned element inside the header that wraps heading and description. The group is not an anatomy part and carries no data-part or testID.
widthSmlayout.maxWidth.prose572px572pxnoSurface width for size sm.
widthMdlayout.maxWidth.content960px960pxnoSurface width for size md: layout.maxWidth.content × 0.75, not a new token. An override replaces the base; the × 0.75 stays in the rule, and the rule multiplies the hook (--ds-dialog-width-md, itself defaulting to the token) rather than the raw token, or an override would never reach it.
widthLglayout.maxWidth.content960px960pxnoSurface width for size lg.
layerlayer.dialog300300noKept as the hook, but it has no effect inside the browser top layer or a native Modal window; it applies to a non-top-layer fallback (position: fixed) only. On rn it is still written as zIndex on the centring container, as AlertDialog, BottomSheet and SidePanel do, so the override is not dead code on that platform — a Modal window simply ignores it.
entermotion.duration.base200ms200msnoScrim fade and surface fade-and-rise, motion.easing.standard: the surface starts space.2 below its resting place (translateY +space.2 → 0) and rises into it; the scrim fades in with the same duration and easing. Runs also when the dialog mounts already open. Instant under reduced motion.
exitmotion.duration.fast120ms120msnoFade only, with motion.easing.exit; the scrim fades out with the same duration and easing as the surface.
focusRingcolor.border.focus #496ded #99b5ffyespart heading: The ring on the heading when it holds focus (tabindex -1), drawn by the Dialog-owned heading wrapper with :has(:focus-visible) — Heading has no focus style of its own. :focus-visible is the intent, not an accident of the programmatic focus: a dialog opened from the keyboard shows the ring, one opened by mouse does not, which is the same rule every other control follows. Web and Lit only: rn draws no ring, the platform’s screen-reader focus indicator stands in.
focusRingWidthborder.width.focus2px2pxyespart heading.

Disclosure

Component doc

BindingTokenLightDarkLockedDescription
triggerColorcolor.foreground #2a2b2f #ecedeeyespart trigger.
triggerBackgroundHovercolor.background.subtle #f6f7f8 #2a2b2fyespart trigger, state hover: Pointer hover and pressed state of the trigger — web and Lit apply it on :hover and :active, RN on onHoverIn/onHoverOut or pressed; suppressed while disabled. The background change is instant (no transition). At rest the trigger has no background: the literal transparent is the sanctioned value there, since no token expresses the absence of a fill.
triggerPaddingBlockspace.sm8px8pxnopart trigger.
triggerPaddingInlinespace.sm8px8pxnopart trigger.
triggerGapspace.28px8pxnopart trigger: Gap between icon and summary.
triggerFontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart trigger.
triggerFontSizefont.size.md16px16pxnopart trigger.
triggerFontWeightfont.weight.medium500500nopart trigger.
triggerLineHeightfont.lineHeight.normal1.51.5nopart trigger: Line height of the summary text, with its own override hook like the other trigger type bindings.
triggerRadiusradius.md4px4pxnopart trigger.
iconcolor.foreground.muted #56585c #bcbec1yespart triggerIcon: Colour of the triggerIcon part: a wrapper the Disclosure owns (a span on web/Lit, a View on native) that carries this colour and the rotation, around the system Icon name="chevron-right" (decorative, aria-hidden), which keeps its own hook and is not restyled. The wrapper lays out as display: inline-flex; flex: none on web and Lit — it centres the 1em glyph without the unitless line-height: 0 trick and leaves the trigger’s height untouched. Rotated 90° when open. On web and Lit the Icon is inline, so it follows the trigger’s font size (including a triggerFontSize override). On native the Icon is not inline (an inline Icon ignores size) and its overrides.size receives the triggerFontSize token (default font.size.md). Mirrored in right-to-left writing on every platform ([dir=rtl] → scaleX(-1) on web, :host(:dir(rtl)) on Lit — a shadow root cannot match an ancestor’s dir attribute, so the :dir() pseudo-class is the Lit floor — composed as scaleX(-1) rotate(90deg) when open; I18nManager.isRTL → chevron-left on native). On native the chevron’s overrides.size is the triggerFontSize binding’s own token path, read from the binding rather than restated, so the two cannot drift.
panelPaddingBlockspace.sm8px8pxnopart panel.
panelPaddingInlinespace.sm8px8pxnopart panel.
panelColorcolor.foreground #2a2b2f #ecedeeyespart panel.
focusRingcolor.border.focus #496ded #99b5ffyespart trigger: On native, where the ring is a border rather than an outline, the border is always present at focusRingWidth and merely transparent while unfocused, so taking focus never shifts the trigger.
focusRingWidthborder.width.focus2px2pxyespart trigger.
minTargetsize.target.min24px24pxyespart trigger: Applied as both a minimum height and a minimum width on the trigger. That alone satisfies target-24px: with the trigger padding the box already exceeds the floor, so no hitSlop is added on native. The trigger is start-aligned in its column (alignSelf flex-start on native, inline-level on web), so the hover fill hugs the summary instead of spanning the parent.
disabledOpacityopacity.disabled0.50.5nopart trigger: Dims the trigger only; the panel keeps full opacity.
transitionmotion.duration.base200ms200msnopart triggerIcon: Chevron rotation only, with motion.easing.standard; instant under reduced motion (on native the first placement of the chevron is always set directly, so nothing animates on first render; later changes animate unless useReducedMotion reports true at that moment). The panel itself does not animate height, and the trigger hover background has no transition.

Divider

Component doc

BindingTokenLightDarkLockedDescription
colorcolor.border #d9dbdd #414246no
thicknessborder.width.thin1px1pxno
spacinglayout.gap.none0px0pxnospacing: none is the off state: it renders no space and sets no hook (not a layout.gap.none value, and an overrides.spacing is not written to the hook either), so an override of this binding does nothing until a spacing value is chosen — overrides change values, never presence.
spacinglayout.gap.tight4px4pxnospacing: none is the off state: it renders no space and sets no hook (not a layout.gap.none value, and an overrides.spacing is not written to the hook either), so an override of this binding does nothing until a spacing value is chosen — overrides change values, never presence.
spacinglayout.gap.normal8px8pxnospacing: none is the off state: it renders no space and sets no hook (not a layout.gap.none value, and an overrides.spacing is not written to the hook either), so an override of this binding does nothing until a spacing value is chosen — overrides change values, never presence.
spacinglayout.gap.loose16px16pxnospacing: none is the off state: it renders no space and sets no hook (not a layout.gap.none value, and an overrides.spacing is not written to the hook either), so an override of this binding does nothing until a spacing value is chosen — overrides change values, never presence.
labelColorcolor.foreground.muted #56585c #bcbec1yespart label: Locked, and deliberately not enforced by Divider: the colour arrives from the composed Text’s tone: muted, which resolves to this same token, and writing a colour rule here would restyle a composed child. The contrast pair is therefore a claim about Text’s muted tone on the page background, which no Divider code can regress.
labelSizefont.size.sm13px13pxnopart label: Passed to the composed Text as its fontSize override, along with fontFamily; Divider does not style the Text itself. The label color and default size come from the Text props size="sm" tone="muted", so Divider writes no label color or size rule of its own. Neither labelSize nor fontFamily has a —ds-divider-* hook on web or Lit: only the bindings the author overrode are passed on, into Text’s overrides, and page CSS reaches the label through Text’s own hooks.
labelGaplayout.gap.normal8px8pxnopart label: Gap between the label and the lines on each side: the gap of the labelled root row (flex gap on web and Lit, the gap style on React Native), not a composed Stack. With no label in effect there is no row, and an override of it does nothing.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart label: Reaches only the composed label Text, through its fontFamily override; the line has no text, so the root sets no font hook.

Feed

Component doc

BindingTokenLightDarkLockedDescription
itemGaplayout.gap.normal8px8pxno
articleInsetlayout.inset.md12px12pxnopart article: Each Card keeps inset="md"; the value is forwarded to the Card’s overrides.paddingBlock and overrides.paddingInline (Card’s inset is an sm/md/lg choice and takes no token).
articleBodyGaplayout.gap.tight4px4pxnopart articleBody: Between the runs of articleBody (unread word, timestamp, position, content); articleBody is a Stack and this is forwarded as its overrides.gap.
unreadBordercolor.control.selectedBackground #3553d2 #496dedyespart article: Start-edge bar on unread articles, paired with the visually-hidden “unread” word. The article part is a Feed-owned wrapper around each Card, and the bar is drawn by that wrapper over the Card’s start edge (mirrored in RTL); the Card itself is not restyled. On React Native there is no overlay without a negative offset, so the wrapper draws a start border instead, which insets the Card by the bar’s width. Read articles reserve nothing in its place, so on that platform an unread row’s Card starts unreadBorderWidth further in than a read one; the shift is accepted rather than padded away, since padding every read row would inset the whole feed for the sake of a few marks.
unreadBorderWidthborder.width.focus2px2pxyespart article.
articleRadiusradius.lg6px6pxnopart article: Corner radius of the Feed-owned article wrapper, matching Card’s own default radius. It matters where the wrapper draws something of its own: on Lit the wrapper is the article and draws the focus ring, which would otherwise be a square around a rounded Card; on web and React Native it only rounds the ends of the unread bar.
timestampColorcolor.foreground.muted #56585c #bcbec1yespart timestamp: Expressed as the composed Text’s tone="muted", not a colour set on the Text.
timestampSizefont.size.xs11px11pxnopart timestamp: The composed Text’s size="xs"; an override is forwarded to the Text’s overrides.fontSize. The timestamp part hook sits on the <time> element inside that Text, so the size applies to the wrapping Text rather than to the part element — as the endMessage and emptyState sizes apply to the Text inside their part wrapper. On React Native the part hook sits on a wrapping View instead (Text takes no testID) and both size="xs" and the override go to the Text inside it, so the part element is again not the styled one.
newItemsOffsetspace.312px12pxnoPadding-block-start of the sticky new-items row (it is the first child, so padding, not a margin). On React Native the row is a View above the FlatList, not a sticky header inside it, so it never scrolls away.
newItemsLayerlayer.raised11nopart newItemsButton: Stacking order of the sticky new-items row over the scrolling articles (web and Lit). On React Native the row is a sibling View above the FlatList with nothing to overlap, so the value changes nothing there; it is still applied as zIndex, which under react-native-web does stack, so an override resolves.
loadingInsetlayout.inset.md12px12pxnopart loadingIndicator: Padding of the Feed-owned loadingIndicator wrapper around the ProgressBar.
endMessageInsetlayout.inset.md12px12pxnopart endMessage: Padding of the Feed-owned endMessage wrapper around the Text; the wrapper carries the part hook.
endMessageColorcolor.foreground.muted #56585c #bcbec1yespart endMessage: Expressed as the composed Text’s tone="muted".
endMessageSizefont.size.sm13px13pxnopart endMessage: The composed Text’s size="sm"; an override is forwarded to the Text’s overrides.fontSize.
emptyStateInsetlayout.inset.md12px12pxnopart emptyState: Padding of the Feed-owned emptyState wrapper around the Text, as endMessageInset.
emptyStateColorcolor.foreground.muted #56585c #bcbec1yespart emptyState: Expressed as the composed Text’s tone="muted".
emptyStateSizefont.size.sm13px13pxnopart emptyState: The composed Text’s size="sm"; an override is forwarded to the Text’s overrides.fontSize.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnoThe root font (a CSS hook inherited by caller content on web and Lit), also forwarded to the overrides.fontFamily of every composed Text (timestamp, end message, empty state, hidden runs, wrapped string content on native) and of the new-items Button. Card’s heading keeps Card’s own font.
focusRingcolor.border.focus #496ded #99b5ffyesChild-owned on web and React Native: the article ring is drawn by the composed Card and the button ring by the composed Button, and the binding only names the token a theme has to keep in step. On Lit the article wrapper is itself the focus target (the Card cannot be focusable there), so Feed draws that ring — an outline in this colour over articleRadius corners, with no offset, mirroring Card’s own focus treatment.
focusRingWidthborder.width.focus2px2pxyesWidth of that ring, on the same elements: child-owned on web and React Native, the wrapper’s outline width on Lit.

Fieldset

Component doc

BindingTokenLightDarkLockedDescription
legendColorcolor.foreground #2a2b2f #ecedeeyespart legend: Realised by the composed legend Text’s default tone; no —ds-fieldset-* hook.
legendSizefont.size.md16px16pxnopart legend: Reaches the legend Text only through its overrides.fontSize; no —ds-fieldset-* hook.
legendWeightfont.weight.medium500500nopart legend: Reaches the legend Text only through its overrides.fontWeight; no —ds-fieldset-* hook.
descriptionTextcolor.foreground.muted #56585c #bcbec1yespart description: Realised by the composed description Text’s muted tone; no —ds-fieldset-* hook.
helperSizefont.size.sm13px13pxnopart description: Description and error text size. Reaches both Texts only through their overrides.fontSize; no —ds-fieldset-* hook.
errorTextcolor.foreground.danger #b51d26 #ff958dyespart errorMessage: Realised by the composed error Text’s danger tone; no —ds-fieldset-* hook.
partGaplayout.gap.tight4px4pxnopart group: Vertical gap between legend, description, fields and error, set on the group root. On web and Lit the does not take part in flex gap, so below the legend it is a margin. One of the two bindings with a —ds-fieldset-* hook (—ds-fieldset-part-gap), because Fieldset’s own elements read it.
fieldsGaplayout.gap.tight4px4pxnopart fields: The composed Stack’s gap. It reaches the Stack only as a token path through the Stack’s own overrides.gap, always sent (the override, else layout.gap.{gap}), and the Stack gets no gap prop or attribute. No —ds-fieldset-* hook (a Fieldset hook would not reach the Stack), and Fieldset does not set the Stack’s CSS hooks (—ds-stack-gap) either; page CSS sizes the gap through the Stack’s own hooks. Because no gap prop is passed, the Stack keeps its own default gap state and its modifier class or attribute still reads normal whatever Fieldset’s gap is: the override wins visually, and the rendered gap is read from the Stack’s hook, never from that class.
fieldsGaplayout.gap.normal8px8pxnopart fields: The composed Stack’s gap. It reaches the Stack only as a token path through the Stack’s own overrides.gap, always sent (the override, else layout.gap.{gap}), and the Stack gets no gap prop or attribute. No —ds-fieldset-* hook (a Fieldset hook would not reach the Stack), and Fieldset does not set the Stack’s CSS hooks (—ds-stack-gap) either; page CSS sizes the gap through the Stack’s own hooks. Because no gap prop is passed, the Stack keeps its own default gap state and its modifier class or attribute still reads normal whatever Fieldset’s gap is: the override wins visually, and the rendered gap is read from the Stack’s hook, never from that class.
fieldsGaplayout.gap.loose16px16pxnopart fields: The composed Stack’s gap. It reaches the Stack only as a token path through the Stack’s own overrides.gap, always sent (the override, else layout.gap.{gap}), and the Stack gets no gap prop or attribute. No —ds-fieldset-* hook (a Fieldset hook would not reach the Stack), and Fieldset does not set the Stack’s CSS hooks (—ds-stack-gap) either; page CSS sizes the gap through the Stack’s own hooks. Because no gap prop is passed, the Stack keeps its own default gap state and its modifier class or attribute still reads normal whatever Fieldset’s gap is: the override wins visually, and the rendered gap is read from the Stack’s hook, never from that class.
disabledOpacityopacity.disabled0.50.5nopart legend: Dims the legend and description only while disabled, applied as opacity on the Fieldset-owned legend and description elements (web/Lit wrappers, RN Fieldset.legend and Fieldset.description Views), never on the Texts. The group error is never dimmed, and the fields dim themselves, so the group root and the fields wrapper are never dimmed. Every element that carries this opacity also reports the disabled state on itself (aria-disabled on web and Lit, and on the two RN wrapper Views under react-native-web): the dim drops the legend below 4.5:1, and that is only permissible because an inactive control is exempt, which nothing can tell without the attribute. It is one of the two bindings with a —ds-fieldset-* hook (—ds-fieldset-disabled-opacity).
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart legend: Reaches the legend, description and error Texts only through their overrides.fontFamily (part names the first; the composition forwards decide where it goes); no —ds-fieldset-* hook.
lineHeightfont.lineHeight.normal1.51.5nopart legend: Reaches the legend, description and error Texts only through their overrides.lineHeight (part names the first; the composition forwards decide where it goes); no —ds-fieldset-* hook.

FocusScope

Component doc

No style bindings.

Form

Component doc

BindingTokenLightDarkLockedDescription
gaplayout.gap.loose16px16pxnoVertical gap between the direct children of the fields part and between fields and actions — the rhythm preset, so a theme’s layout.rhythm reaches every form. A Stack given as children spaces its own fields with its own gap; Form does not reach inside it.
errorSummaryBordercolor.border.danger #b51d26 #ff958dnopart errorSummary.
errorSummaryTextcolor.foreground.danger #b51d26 #ff958dyespart errorSummary.
errorSummaryBackgroundcolor.background.subtle #f6f7f8 #2a2b2fyespart errorSummary.
errorSummaryBorderWidthborder.width.thin1px1pxnopart errorSummary.
errorSummaryRadiusradius.md4px4pxnopart errorSummary.
errorSummaryLineHeightfont.lineHeight.normal1.51.5yespart errorSummary: Line height of the summary box, inherited by the item Links. Without it they take the document’s normal and each link’s box falls under the 24px target floor — Form may not give the Links a target of their own, so the box sets the body line height instead and the Links inherit it. Locked because it is the only thing keeping those targets reachable.
errorSummaryPaddingspace.md12px12pxnopart errorSummary: Inset on all four sides of the summary box.
errorSummaryGaplayout.gap.tight4px4pxnopart errorSummary: Gap between the heading and the list and between list items. Inside the errorSummary box a Stack holds the heading and the list, and the list is a second Stack (element ul on web and Lit) with no markers and no indent; Form forwards this binding to both Stacks’ overrides.gap (the overrides property on Lit) and has no —ds-form-* hook for it, as Input’s helperSize has none. The item Links are passed to the list Stack bare: Stack wraps each child in its own li. Having no hook of its own, it is the one binding a consumer cannot reach from their own CSS — they size it through the Stacks’ own hooks, as with Fieldset’s fieldsGap.

Heading

Component doc

BindingTokenLightDarkLockedDescription
fontFamilyfont.family.headingsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifno
fontWeightfont.weight.semibold600600no
fontSizefont.size.4xl40px40pxno
fontSizefont.size.3xl33px33pxno
fontSizefont.size.2xl28px28pxno
fontSizefont.size.xl23px23pxno
fontSizefont.size.lg19px19pxno
fontSizefont.size.md16px16pxno
lineHeightfont.lineHeight.tight1.21.2no
colorcolor.foreground.strong #000000 #ffffffyesLocked by the AAA pair, so it is absent from the overrides type; it keeps its --ds-heading-color hook, defaulting to var(--color-foreground-strong), and the rule reads the hook, as every locked binding does.
marginBlockEndspace.sm8px8pxnoSpace below the heading (marginBottom on React Native — the one margin the system allows, because a heading owns the gap to its own first paragraph). It is unconditional: a container that owns its own rhythm turns it off with overrides={{ marginBlockEnd: 'space.0' }}, as Table does. That override is written out as a margin of 0, never omitted.

Icon

Component doc

BindingTokenLightDarkLockedDescription
sizefont.size.xs11px11pxnoThe glyph box is a square of 1em; size sets that em (font-size on the element), so the box tracks the type scale. With inline, font-size is inherited instead and overrides.size is a no-op (the hook is still set, for consistency, and on web the ds-icon--{size} modifier class stays applied; .ds-icon--inline is declared later and wins). A non-inline overrides.size is written as the --ds-icon-size hook inline, so it outranks the ds-icon--{size} modifier’s declaration of the same hook. React Native has no hook, so there the override is simply ignored while inline. Non-inline icons are display inline-block with vertical-align middle (the Lit host is inline-flex instead; see its note); inline ones sit at vertical-align -0.125em.
sizefont.size.sm13px13pxnoThe glyph box is a square of 1em; size sets that em (font-size on the element), so the box tracks the type scale. With inline, font-size is inherited instead and overrides.size is a no-op (the hook is still set, for consistency, and on web the ds-icon--{size} modifier class stays applied; .ds-icon--inline is declared later and wins). A non-inline overrides.size is written as the --ds-icon-size hook inline, so it outranks the ds-icon--{size} modifier’s declaration of the same hook. React Native has no hook, so there the override is simply ignored while inline. Non-inline icons are display inline-block with vertical-align middle (the Lit host is inline-flex instead; see its note); inline ones sit at vertical-align -0.125em.
sizefont.size.md16px16pxnoThe glyph box is a square of 1em; size sets that em (font-size on the element), so the box tracks the type scale. With inline, font-size is inherited instead and overrides.size is a no-op (the hook is still set, for consistency, and on web the ds-icon--{size} modifier class stays applied; .ds-icon--inline is declared later and wins). A non-inline overrides.size is written as the --ds-icon-size hook inline, so it outranks the ds-icon--{size} modifier’s declaration of the same hook. React Native has no hook, so there the override is simply ignored while inline. Non-inline icons are display inline-block with vertical-align middle (the Lit host is inline-flex instead; see its note); inline ones sit at vertical-align -0.125em.
sizefont.size.lg19px19pxnoThe glyph box is a square of 1em; size sets that em (font-size on the element), so the box tracks the type scale. With inline, font-size is inherited instead and overrides.size is a no-op (the hook is still set, for consistency, and on web the ds-icon--{size} modifier class stays applied; .ds-icon--inline is declared later and wins). A non-inline overrides.size is written as the --ds-icon-size hook inline, so it outranks the ds-icon--{size} modifier’s declaration of the same hook. React Native has no hook, so there the override is simply ignored while inline. Non-inline icons are display inline-block with vertical-align middle (the Lit host is inline-flex instead; see its note); inline ones sit at vertical-align -0.125em.
sizefont.size.xl23px23pxnoThe glyph box is a square of 1em; size sets that em (font-size on the element), so the box tracks the type scale. With inline, font-size is inherited instead and overrides.size is a no-op (the hook is still set, for consistency, and on web the ds-icon--{size} modifier class stays applied; .ds-icon--inline is declared later and wins). A non-inline overrides.size is written as the --ds-icon-size hook inline, so it outranks the ds-icon--{size} modifier’s declaration of the same hook. React Native has no hook, so there the override is simply ignored while inline. Non-inline icons are display inline-block with vertical-align middle (the Lit host is inline-flex instead; see its note); inline ones sit at vertical-align -0.125em.
colorcolor.foreground #2a2b2f #ecedeenoThe default is inherit (currentColor): glyphs take the text color, so a Button, Link or Alert colors them for free, and color.foreground is only what inheritance resolves to at the root. The CSS hook therefore defaults to currentColor, not to the token — defaulting it to the token would break that inheritance — and color.foreground is what it resolves to at the root. overrides.color sets an explicit color. On React Native, where there is no currentColor, the order is: the color prop, else overrides.color, else the enclosing Text’s TextStyleContext colour whenever the glyph is nested in a Text (nesting alone is enough; inline governs only the size; any TextStyleContext provider with nested: true counts, so a Heading or Link colours it too), else color.foreground.
strokeWidthborder.width.focus2px2pxyesStroke thickness of line glyphs (check, dash, chevrons, close, plus, minus, external, search, arrows, calendar, menu), in screen pixels at every size (vector-effect non-scaling-stroke), so glyphs stay legible at xs. Filled glyphs have no stroke: each is one evenodd path whose inner mark is a hole. All three platforms; on React Native through react-native-svg. The binding is locked — it sits on a token that carries an accessibility guarantee — so it is not a member of the overrides type, though the --ds-icon-stroke-width hook still exists for a consumer’s own CSS. A composite that binds its own indicator stroke to the same token (Checkbox’s indicatorStroke) is locked for the same reason and forwards nothing: the two already resolve to one value. react-native-svg honours vector-effect only under react-native-web, so web passes the raw token with vectorEffect="non-scaling-stroke" and native passes the token scaled into the 16-grid at the rendered size, which is what keeps a native stroke from thickening at xl.

Input

Component doc

BindingTokenLightDarkLockedDescription
backgroundcolor.background #ffffff #17181byes
foregroundcolor.foreground #2a2b2f #ecedeeyes
placeholdercolor.foreground.muted #56585c #bcbec1yes
bordercolor.border.strong #939599 #707176yes
borderFocuscolor.border.focus #496ded #99b5ffyes
borderInvalidcolor.border.danger #b51d26 #ff958dno
errorTextcolor.foreground.danger #b51d26 #ff958dyesRealised by the composed Text’s danger tone. Locked, so absent from the overrides type, but it keeps its --ds-input-error-text hook on the root, defaulting to var(--color-foreground-danger), as every locked binding does.
descriptionTextcolor.foreground.muted #56585c #bcbec1yespart description: Realised by the composed Text’s muted tone. Keeps its --ds-input-description-text hook on the root, defaulting to var(--color-foreground-muted), as for errorText.
borderWidthborder.width.thin1px1pxno
radiusradius.md4px4pxno
paddingInlinespace.md12px12pxno
paddingInline (size=sm)space.28px8pxno
paddingBlockspace.sm8px8pxno
paddingBlock (size=sm)space.14px4pxno
partGapspace.14px4pxnoVertical gap between label, description, field, and error.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifno
fontSizefont.size.sm13px13pxnoThe field text and the label, so the label follows size; description and error use helperSize.
fontSizefont.size.md16px16pxnoThe field text and the label, so the label follows size; description and error use helperSize.
labelWeightfont.weight.medium500500nopart label: On web and Lit the native label’s weight. On native the label is the system Text with weight="medium", its size following size, and an override is forwarded as that Text’s fontWeight.
helperSizefont.size.sm13px13pxnoDescription and error text size. Reaches them only through the composed Text’s overrides (fontSize), with fontFamily and lineHeight forwarded the same way; it has no —ds-input-* hook, so page CSS sizes helper text through Text’s own hooks. The description and error Texts are size="sm", matching this default, with the override forwarded on top. fontFamily and lineHeight keep their root hooks as well, for the label and the field.
lineHeightfont.lineHeight.normal1.51.5no
minTargetsize.target.comfortable44px44pxyes
minTargetSmsize.target.min24px24pxyesThe field height floor at size sm. Locked like minTarget (an accessibility floor, not an override), so the sm floor is always size.target.min.
focusRingWidthborder.width.focus2px2pxyesReplaces borderWidth when focused (the field’s border IS its focus ring — no outline); when the field is both invalid and focused the danger color stays and only the width changes, so the error is never hidden by focus. Padding shrinks by the difference on both axes — inline and block, since compensating only the inline one would still move the field vertically — and the compensation is clamped at zero, so a borderWidth override wider than this locked width leaves the padding alone rather than eating into it.
disabledOpacityopacity.disabled0.50.5noApplied to the whole field group (label, description, field, error), as Button dims the whole control. The group is the root wrapper that also carries partGap; it is not an anatomy part (web: the div with data-ds; Lit: an unnamed wrapper in the shadow root; native: the outer View with testID=“Input”).
transitionmotion.duration.fast120ms120msnoBorder color on focus and invalid, with motion.easing.standard. Border width and padding change instantly (no layout animation). Instant under reduced motion. An override changes the duration only; the easing stays motion.easing.standard. Native swaps instantly, so the binding stays in the native overrides type for parity and an override of it has no effect there.

Landmark

Component doc

No style bindings.

Component doc

BindingTokenLightDarkLockedDescription
colorcolor.link #3553d2 #99b5ffyes
colorHovercolor.link.hover #243ab3 #c2d4ffyesstate hover: Pointer hover only on web and Lit (not :active), as a plain :hover rule with no @media (hover: hover) guard; a sticky hover color after a tap is an accepted, proven pair. On native, which has no hover, the pressed color.
colorVisitedcolor.link.visited #162391 #dee8ffyesWeb and Lit only; native has no visited state.
underlineThicknessborder.width.thin1px1pxnoText-decoration thickness; the underline is always present at rest.
underlineOffsetspace.14px4pxno
externalIconGapspace.14px4pxnopart externalIcon: Gap before the trailing icon, applied as margin-inline-start on Link’s own externalIcon wrapper span (inline text has no Stack to use), never on the Icon inside it. The icon is 1em of the surrounding font size (no token: it scales with the text). On React Native nested Text ignores margins, so the gap is a single literal space and this binding is not applied. An override of it is written only while external is true, the only time the binding is in effect.
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyes
focusRingRadiusradius.sm2px2pxyesApplied (as border-radius) only under :focus-visible, so a resting inline anchor is not rounded.
focusRingOffsetborder.width.focus2px2pxyesoutline-offset of the focus ring on web and Lit, deliberately the same token as its width, so the two are indistinguishable at every theme until one of them is given a token of its own. Not applied on native, where the ring is the platform’s own.
transitionmotion.duration.fast120ms120msnoColor transition on hover, with motion.easing.standard. Only color transitions; the underline follows it through currentColor.

Listbox

Component doc

BindingTokenLightDarkLockedDescription
surfacecolor.background #ffffff #17181byes
bordercolor.border.strong #939599 #707176noOnly when not embedded; inside a popup the popup owns the border.
borderInvalidcolor.border.danger #b51d26 #ff958dnoReplaces border while invalid, only when not embedded; an embedded list conveys invalid by aria-invalid and the error message alone.
partGapspace.14px4pxnoVertical gap between the list and errorMessage. Both sit in the root wrapper, which is not an anatomy part (web: the div with data-ds, whose child is the role=listbox list part, since a listbox may only contain options and groups; Lit: an unnamed wrapper in the shadow root; native: the outer View with testID=“Listbox”).
errorTextcolor.foreground.danger #b51d26 #ff958dyespart errorMessage: Realised by the composed Text’s tone danger; declares no hook of its own.
borderWidthborder.width.thin1px1pxno
radiusradius.md4px4pxno
listPaddingspace.14px4pxnopart list.
optionPaddingBlockspace.sm8px8pxnopart option.
optionPaddingInlinespace.md12px12pxnopart option.
optionGaplayout.gap.normal8px8pxnopart option: The row’s horizontal gap: between check, icon and the label column. The description sits under the label in that column and takes no gap from it — this much space between a label and its own sub-line would read as two rows — and rows themselves have no gap between them.
optionRadiusradius.sm2px2pxnopart option.
optionColorcolor.foreground #2a2b2f #ecedeeyespart option.
optionDescriptionColorcolor.foreground.muted #56585c #bcbec1yespart optionDescription.
optionDescriptionSizefont.size.sm13px13pxnopart optionDescription.
optionActiveBackgroundcolor.background.subtle #f6f7f8 #2a2b2fyespart option, state active: The focused/active option (keyboard or hover). Selection is shown by the check and weight, so active and selected are never confused.
optionWeightfont.weight.regular400400nopart option: Unselected options; selected ones use optionSelectedWeight.
optionSelectedWeightfont.weight.medium500500nopart option, state selected: Wins over optionWeight on a selected row.
optionSelectedCheckcolor.control.selectedBackground #3553d2 #496dedyespart optionCheck: The check icon on selected options, in the selected-control fill (3:1 on both surfaces by derivation). Only with multiple, where the slot is present on every row (invisible when unselected) so labels align. Single-select has no check slot and shows selection by optionSelectedWeight alone, never a row fill, since the fill is the active state.
groupLabelColorcolor.foreground.muted #56585c #bcbec1yespart groupLabel.
groupLabelSizefont.size.xs11px11pxnopart groupLabel.
groupLabelWeightfont.weight.semibold600600nopart groupLabel.
groupLabelPaddingBlockspace.14px4pxnopart groupLabel: Block padding only; the group label’s inline padding is optionPaddingInline, so it lines up with the option labels.
emptyColorcolor.foreground.muted #56585c #bcbec1yespart emptyState: Realised by the composed Text’s tone muted (also for copy.loading); declares no hook of its own.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifno
fontSizefont.size.md16px16pxno
lineHeightfont.lineHeight.normal1.51.5noA unitless multiplier, which is what makes fontSize × lineHeight in the row formula a valid length.
minTargetsize.target.min24px24pxyes
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyesNever changes layout. Web and Lit draw it as an outline on the focused list. Native draws it as a row border that is always reserved (transparent unless the row is focused), so native rows are 2 × focusRingWidth taller than the Behavior formula, and maxVisible counts that.
disabledOpacityopacity.disabled0.50.5no
typeaheadResetmotion.duration.loop800ms800msnopart list: How long typed characters accumulate before the typeahead buffer clears; read at runtime from the list’s computed custom property (ms or s parsed to milliseconds). When it cannot be read (no token stylesheet, as in jsdom) the buffer clears immediately, so only single-character typeahead works. React Native has no typeahead and accepts it in its overrides type for parity only.

Component doc

BindingTokenLightDarkLockedDescription
surfacecolor.overlay.surface #ffffff #2a2b2fyespart popup.
bordercolor.border #d9dbdd #414246nopart popup.
borderWidthborder.width.thin1px1pxnopart popup: The popup border, and also the thickness of each separator line.
shadowshadow.overlay0px 8px 24px 0px #0000002e0px 8px 24px 0px #00000080nopart popup.
radiusradius.md4px4pxnopart popup.
popupPaddingspace.14px4pxnopart popup: Inset around the list so item hover backgrounds do not touch the border.
popupOffsetspace.14px4pxnopart popup: Gap between trigger and popup, on the side facing the trigger; the flip check includes it, and a flipped popup keeps the same gap on its new side. On web and Lit the token length is read in px from the popup’s resolved hook when the popup opens and repositions (rem × root font size), and an unresolvable value is 0 — the popup then sits flush against the trigger rather than at a hard-coded fallback; rn uses the resolved theme value.
typeaheadResetmotion.duration.loop800ms800msnopart popup: How long typed characters accumulate before the typeahead buffer clears; read at runtime from the popup. An unresolvable value clears the buffer after every keypress, degrading typeahead to single characters rather than letting it accumulate forever.
maxHeightlayout.maxWidth.prose572px572pxnopart popup: The popup’s block size never exceeds the viewport height minus gutter at each edge (2 × gutter), nor this token; beyond that the list scrolls. The cap is on the popup itself, so its border and popupPadding are inside it (rn: the popup View, with the ScrollView inside). The token is a measure, not a height: it is reused deliberately so a popup is never taller than a comfortable reading measure, which also means retheming layout.maxWidth.prose moves this cap and phoneBreakpoint together.
gutterlayout.gutter24px24pxnopart popup: Space kept between the popup and each viewport edge: it caps maxHeight (viewport height − 2 × gutter), bounds the inline shift, and is kept from the block edges too, so a flipped popup anchored to a trigger near the top or bottom is clamped rather than sitting against the edge. Read in px the same way as popupOffset.
minWidthspace.2080px80pxnopart popup: Popup is at least this wide (200px at comfortable density) and at least the trigger width. An override replaces the base; the × 2.5 stays in the rule. The trigger width is only known at runtime, so it is measured when the popup opens and repositions and the popup gets max(<the × 2.5 rule>, <trigger width>px) — on web and Lit the measurement is published as a platform-private custom property (--ds-menu-trigger-width, 0px in anchor mode) that the min-inline-size rule maxes against; it is not an overridable binding and is not in the overrides type. With anchor there is no trigger-width floor, and the anchor’s own width is not one either: the popup falls back to the × 2.5 rule. On rn the width is known only from onLayout, so the popup is held at opacity 0 until the measurement and the layout have both reported, rather than being placed from the floor for one frame and jumping when an end placement learns its real width.
phoneBreakpointlayout.maxWidth.prose572px572pxyesrn only: a window width at or below this presents as ActionSheet, above it as the anchored popup; the breakpoint is read from the theme token, not per instance.
itemPaddingBlockspace.sm8px8pxnopart item: Also pads the groupLabel row so the label lines up with the items.
itemPaddingInlinespace.md12px12pxnopart item: Also pads the groupLabel row so the label lines up with the items.
itemGaplayout.gap.normal8px8pxnopart item.
itemRadiusradius.sm2px2pxnopart item.
itemHovercolor.background.subtle #f6f7f8 #2a2b2fyespart item, state hover: Pointer hover and keyboard focus share this highlight, and on rn so does press (hover tracked with onHoverIn/onHoverOut).
itemColorcolor.foreground #2a2b2f #ecedeeyespart item.
itemDangerColorcolor.foreground.danger #b51d26 #ff958dyespart item.
groupLabelColorcolor.foreground.muted #56585c #bcbec1yespart groupLabel.
groupLabelSizefont.size.xs11px11pxnopart groupLabel.
groupLabelWeightfont.weight.semibold600600nopart groupLabel.
shortcutColorcolor.foreground.muted #56585c #bcbec1yespart itemShortcut.
shortcutSizefont.size.sm13px13pxnopart itemShortcut.
separatorcolor.border #d9dbdd #414246nopart separator.
separatorMarginspace.14px4pxnopart separator.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart popup.
fontSizefont.size.md16px16pxnopart popup.
lineHeightfont.lineHeight.normal1.51.5nopart popup.
minTargetsize.target.min24px24pxyespart item.
layerlayer.dropdown100100nopart popup: z-index of the position: fixed popup (web portal, Lit fallback); it has no effect inside the browser top layer (Lit popover=“manual”) or a native Modal window, where it is still written on the popup for parity rather than dropped.
entermotion.duration.fast120ms120msnopart popup: Fade and an enterDistance slide from the side facing the trigger after any flip, with motion.easing.standard; instant under reduced motion. A popup placed below starts enterDistance above its resting place and settles down into it; one placed above starts that far below and rises. Popup only: the item highlight changes instantly — itemHover carries state: hover but is exempt from the transition every other hover binding gets, because the highlight also tracks keyboard focus.
enterDistancespace.14px4pxnopart popup: How far the popup slides in on enter; separate from popupOffset, so overriding the gap does not change the slide.
focusRingcolor.border.focus #496ded #99b5ffyespart item.
focusRingWidthborder.width.focus2px2pxyespart item.

Meter

Component doc

BindingTokenLightDarkLockedDescription
trackcolor.background.strong #ecedee #414246yespart track.
fillcolor.status.info.icon #405bb6 #99b5ffyespart fill: The icon step of each status hue is the one guaranteed 3:1 against the page background, which makes it the right non-text fill.
fillcolor.status.success.icon #00791d #76d07ayespart fill: The icon step of each status hue is the one guaranteed 3:1 against the page background, which makes it the right non-text fill.
fillcolor.status.warning.icon #825b00 #e6ac3fyespart fill: The icon step of each status hue is the one guaranteed 3:1 against the page background, which makes it the right non-text fill.
fillcolor.status.danger.icon #b51d26 #ff958dyespart fill: The icon step of each status hue is the one guaranteed 3:1 against the page background, which makes it the right non-text fill.
trackHeightspace.28px8pxnopart track.
radiusradius.full999px999pxnopart track: Rounds the track and the fill ends: the track clips the fill (overflow hidden) and the fill carries the same radius on every platform, so the leading end of a partial fill is rounded too. There is deliberately no separate fill radius — the fill reads the same hook, and an override moves both ends together.
labelColorcolor.foreground #2a2b2f #ecedeeyespart label: Realised by the label Text’s tone default; no hook of its own.
labelSizefont.size.sm13px13pxnopart label: Forwarded to the label Text’s fontSize override.
labelWeightfont.weight.medium500500nopart label: Forwarded to the label Text’s fontWeight override.
valueColorcolor.foreground.muted #56585c #bcbec1yespart valueText: Realised by the value Text’s tone muted; no hook of its own.
valueSizefont.size.sm13px13pxnopart valueText: Forwarded to the value Text’s fontSize override. The value text weight is Text’s regular and is not a binding.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart header: Forwarded to both Texts’ fontFamily overrides; never styles them directly. part: header only says it covers both Texts in the header row: the header element itself takes no style and no hook from it.
lineHeightfont.lineHeight.normal1.51.5nopart header: Forwarded to both Texts’ lineHeight overrides; never styles them directly. part: header only says it covers both Texts in the header row: the header element itself takes no style and no hook from it.
partGapspace.14px4pxnopart container: Vertical gap between the label row (header) and the track.
labelGapspace.28px8pxnopart header: Horizontal gap between the label and the value text in the header row.
transitionmotion.duration.base200ms200msnopart fill: Fill inline-size change, with motion.easing.standard; instant under reduced motion. The rule that a width change coming only from layout (first layout, a resize) snaps is a React Native rule: there the fill is measured pixels, so layout moves the animated value. On web and Lit the fill is a percentage, so a resize never changes the declared width and nothing animates — the rule holds there without any code. On native a fraction change arriving in the same commit as a resize is indistinguishable from a pure resize, so it snaps too.

NumberInput

Component doc

BindingTokenLightDarkLockedDescription
backgroundcolor.background #ffffff #17181byes
foregroundcolor.foreground #2a2b2f #ecedeeyes
placeholdercolor.foreground.muted #56585c #bcbec1yes
bordercolor.border.strong #939599 #707176yes
borderFocuscolor.border.focus #496ded #99b5ffyespart field: The focus ring is drawn on the bordered field part while the input matches :focus-visible; the input itself has no border. As Input, the ring is the field’s border (width swapped to focusRingWidth, no outline), and both paddingInline and paddingBlock shrink by the width difference so the field does not grow or shift. The two bindings sit on different parts, so the compensation is split: the field publishes the resolved border width as a custom property, subtracts it from its own padding-inline-start (and from padding-inline-end only in the hidden-stepper case, where that padding exists at all), and the input subtracts the inherited delta from its padding-block.
borderInvalidcolor.border.danger #b51d26 #ff958dno
borderWidthborder.width.thin1px1pxno
radiusradius.md4px4pxno
paddingInlinespace.md12px12pxnopart field: The field’s inline-start padding; its inline-end padding only when the steppers are hidden, since the stepper Buttons sit flush at the end.
paddingInline (size=sm)space.28px8pxnopart field: The field’s inline-start padding; its inline-end padding only when the steppers are hidden, since the stepper Buttons sit flush at the end.
paddingBlockspace.sm8px8pxno
paddingBlock (size=sm)space.14px4pxno
affixColorcolor.foreground.muted #56585c #bcbec1yesPrefix and suffix text.
affixGaplayout.gap.tight4px4pxnoBetween an affix and the value; with steppers shown, the suffix also keeps this gap before the stepper divider.
stepperGaplayout.gap.none0px0pxnoThe two stepper Buttons sit flush at the end of the field, separated from the input by a hairline. There is no divider between the two Buttons; their icons separate them.
stepperDividercolor.border #d9dbdd #414246no
stepperDividerWidthborder.width.thin1px1pxnoWidth of the hairline between the input and the steppers (the inline-start border of the element wrapping both stepper Buttons).
partGapspace.14px4pxno
labelWeightfont.weight.medium500500nopart label.
helperSizefont.size.sm13px13pxno
descriptionTextcolor.foreground.muted #56585c #bcbec1yespart description.
errorTextcolor.foreground.danger #b51d26 #ff958dyes
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifno
fontSizefont.size.sm13px13pxno
fontSizefont.size.md16px16pxno
lineHeightfont.lineHeight.normal1.51.5no
minTargetsize.target.comfortable44px44pxyes
minTargetSmsize.target.min24px24pxyesThe field height floor at size sm. The stepper Buttons are Button size sm at both field sizes and have no target floor of their own: each stretches to the field’s height, so at size sm the applicable floor is this 24px one. That is the right floor for them — a stepper is a pointer convenience whose keyboard and screen-reader equivalent (the arrow keys, the adjustable actions) is always available, which is the WCAG 2.5.8 AA exception the 44px comfortable target does not have to cover.
focusRingWidthborder.width.focus2px2pxyespart field.
disabledOpacityopacity.disabled0.50.5noApplied to the label, description, input and affix parts of a disabled field (the description Text through a wrapper NumberInput owns, which carries its data-part, never a rule on the Text — so [data-part="description"] is that wrapper and aria-describedby points at it, while the composition entry names what the wrapper contains); the field frame (border, background) and the errorMessage are not dimmed. Every element that dims also carries aria-disabled="true" — the root group and the input, on React Native the root View and the TextInput beside accessibilityState.disabled, since react-native-web drops accessibilityState — so a contrast checker resolves the dimmed label and value to an inactive component and applies the WCAG 1.4.3 exemption instead of reporting a failure. Not to an element containing the steppers: the stepper Buttons receive disabled (at a bound, or when the field is disabled) and dim once through their own disabled style.

Popover

Component doc

BindingTokenLightDarkLockedDescription
surfacecolor.overlay.surface #ffffff #2a2b2fyes
bordercolor.border #d9dbdd #414246no
borderWidthborder.width.thin1px1pxno
shadowshadow.overlay0px 8px 24px 0px #0000002e0px 8px 24px 0px #00000080no
radiusradius.md4px4pxno
insetlayout.inset.md12px12pxnoPadding of the panel surface itself. The body Box gets no inset of its own, so nothing is forwarded to Box.
partGaplayout.gap.normal8px8pxnoBetween the header row and the body, and between the heading and the close button inside the header row. The header is a row: the heading fills it and the close button sits at its inline end (still at the end when there is no heading), so no padding is reserved for the button. With neither a heading nor a close button (dismissible: false and no heading) the header row is not rendered at all, so this gap applies to nothing and the body meets the panel inset directly.
offsetspace.28px8pxnoGap between trigger and panel. Web and Lit apply it as a margin on the panel side facing the trigger (the --ds-popover-offset hook), which the flip check reads back from the computed style — a positioning offset, not sibling spacing. Under top/left positioning the margin moves the box only when the facing side is the panel’s top or left edge (data-side bottom or right); when it is its bottom or right edge (data-side top or left), the position math subtracts the read-back value itself, so the gap is the same on all four sides. React Native adds it to the position measured from the trigger.
arrowSizespace.28px8pxnopart arrow: A square rotated 45°, filled with surface and edged with border at borderWidth on its two outer sides — the two the 45° rotation turns outward: facing edge top → top and left, bottom → bottom and right, left → left and bottom, right → top and right — centered on the panel edge facing the trigger on every platform. “Centered” is a plain 50% of that edge for all eight placements, which is exact for the four plain sides and an approximation at the four corners, where a narrow trigger can leave the arrow pointing past it; clamping it toward the trigger centre would need a measuring pass this component does not make. The square’s center sits on the centerline of the panel’s border on that edge (half borderWidth in from the outer edge), drawn above the panel, so its edged sides meet the panel border and its fill covers the border beneath its base.
maxWidthlayout.maxWidth.prose572px572pxnoThe panel’s maximum inline size, itself clamped to the viewport: min(maxWidth, 100vw − 2 × gutter), since flip-shift cannot bring a panel wider than the viewport into view.
gutterlayout.gutter24px24pxnoSpace kept between the panel and each viewport edge when the panel flips or shifts, and the clamp maxWidth reads. It flips only when the opposite side fits within it; otherwise it stays and shifts along the cross axis alone, so the main axis may overflow rather than the panel being squeezed or made to scroll.
breakpointlayout.maxWidth.prose572px572pxyesReact Native only: at a window width <= this token the popover renders as a BottomSheet. The breakpoint is read from the theme token, not per instance. Web and Lit have no narrow presentation.
layerlayer.dropdown100100noThe overlay block’s layer: popover is the category (the top layer via the Popover API or showModal()); this binding stays as the hook but has no effect inside the browser top layer or a native Modal, and applies to the position: fixed fallback and the React Native anchor view.
entermotion.duration.fast120ms120msnoFade and an enterDistance slide from the trigger side, with motion.easing.standard; instant under reduced motion. The slide follows the resolved physical side after right-to-left mirroring (data-side on web and Lit, the resolved edge on React Native): the panel starts enterDistance toward the trigger and moves away from it to rest.
enterDistancespace.14px4pxnoThe length of the enter slide from the trigger side.
exitmotion.duration.fast120ms120msnoFade out with motion.easing.exit and no slide (the enter slide does not reverse); instant under reduced motion.
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyes

ProgressBar

Component doc

BindingTokenLightDarkLockedDescription
trackcolor.background.strong #ecedee #414246nopart track.
fillcolor.control.selectedBackground #3553d2 #496dedyespart fill: Neutral fill. The selected-control color is guaranteed 3:1 against the page.
fillSuccesscolor.status.success.icon #00791d #76d07ayespart fill.
fillDangercolor.status.danger.icon #b51d26 #ff958dyespart fill.
trackHeightspace.28px8pxnopart track.
radiusradius.full999px999pxnopart track: Rounds the track and the fill ends; the track clips the fill (overflow hidden).
labelColorcolor.foreground #2a2b2f #ecedeeyespart label: Realised by the label Text’s tone default; no hook of its own.
labelSizefont.size.sm13px13pxnopart label: Forwarded to the label Text’s fontSize override.
labelWeightfont.weight.medium500500nopart label: Forwarded to the label Text’s fontWeight override.
valueColorcolor.foreground.muted #56585c #bcbec1yespart valueText: Realised by the value Text’s tone muted; no hook of its own.
valueSizefont.size.sm13px13pxnopart valueText: Forwarded to the value Text’s fontSize override.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart header: Forwarded to both Texts’ fontFamily overrides; never styles them directly.
lineHeightfont.lineHeight.normal1.51.5nopart header: Forwarded to both Texts’ lineHeight overrides; never styles them directly.
partGapspace.14px4pxnopart container: Vertical gap between the label row (header) and the track.
labelGapspace.28px8pxnopart header: Horizontal gap between the label and the value text in the header row. The label’s wrapper shrinks (flexShrink 1, min inline size 0) so a long label wraps inside the row instead of pushing the value text out of it — layout, with no token of its own, as Meter does.
transitionmotion.duration.base200ms200msnopart fill: Fill inline-size change with motion.easing.standard; instant under reduced motion. An override changes the duration only; the easing is read from the token and has no hook (the sweep’s easing is sweepEasing).
indeterminateLoopmotion.duration.loop800ms800msnopart fill: The indeterminate sweep: a fill one third of the track width (the one-third ratio is geometry, not a token; a literal is allowed for it) travelling from the inline start to the inline end (right to left in RTL) and repeating, starting and ending wholly outside the track — from -(sweep width) to +(track width) on the inline axis, mirrored to +(sweep width)…-(track width) in RTL. Under reduced motion there is no sweep: the fill is drawn static and full-width at indeterminateReducedOpacity, keeping its tone color.
indeterminateReducedOpacityopacity.disabled0.50.5nopart fill: Opacity of the static full-width fill that replaces the sweep under reduced motion, on every platform. A binding rather than a literal so a theme can make the reduced-motion form more or less prominent.
sweepEasingmotion.easing.standardcubic-bezier(0.2, 0, 0, 1)cubic-bezier(0.2, 0, 0, 1)nopart fill: Easing of each indeterminate sweep on every platform. The fill is off the track at both ends of the loop, so the eased restart has no visible seam.

RadioGroup

Component doc

BindingTokenLightDarkLockedDescription
controlBackgroundcolor.control.background #ffffff #2a2b2fyespart radio.
controlBordercolor.control.border #939599 #707176yespart radio.
controlBorderWidthborder.width.thin1px1pxnopart radio.
controlSelectedBackgroundcolor.control.selectedBackground #3553d2 #496dedyespart radio: Selected border color; the fill stays controlBackground and the dot is drawn inside.
indicatorcolor.control.selectedBackground #3553d2 #496dedyespart radioIndicator: The centre dot, controlSize minus 2 × indicatorInset in diameter, centred, and always a circle (it does not follow controlRadius). Its size does not change on focus: the thicker focus border eats into the inset, and the dot still fits whenever indicatorInset ≥ focusRingWidth (true at the default tokens), so it never shrinks. One rule on every platform: the dot is a real node, so it carries the part name everywhere. Web and Lit draw it as a span the RadioGroup owns inside the option’s label, positioned over the appearance:none input and pointer-events: none, with data-part="radioIndicator" (and part on Lit) — not a pseudo-element of the input, because Firefox does not render ::before or ::after on form controls and the dot would simply be missing there, leaving selection conveyed by border colour alone; on native it is a View inside the drawn control carrying testID=“RadioGroup.radioIndicator”.
indicatorInsetspace.14px4pxnopart radioIndicator: Gap between the control’s outer edge and the dot on each side; sets the dot diameter (controlSize − 2 × indicatorInset). On React Native the border is drawn inside the box and the dot does not shrink, so an inset below focusRingWidth clips the dot while focused: focusRingWidth is the floor this binding should be overridden above.
controlBorderInvalidcolor.border.danger #b51d26 #ff958dnopart radio: Painted on every radio while the group is invalid, selected ones included — it sits above controlSelectedBackground in the border-colour order and below nothing: focus does not replace it. Border colour precedence is invalid, then selected, then rest, the same order as Checkbox, and a focused invalid radio keeps the danger colour while only the width changes to focusRingWidth.
controlSizespace.520px20pxnopart radio: Outer size of the drawn circle, border included (border-box); it stays this size when the focus border thickens.
controlRadiusradius.full999px999pxnopart radio: Corner radius of the drawn control only; the dot stays a circle.
optionPaddingBlockspace.14px4pxnoBlock padding of each option row (the row wrapper holding radio, radioLabel and radioDescription; not an anatomy part, so the binding has no part and the row carries no data-part/testID). The row still meets minTarget.
optionTextGapspace.14px4pxnopart radioDescription: Vertical gap between an option’s radioLabel and its radioDescription.
optionGapspace.28px8pxnoHorizontal gap between radio and its label.
listGapspace.28px8pxnoGap between options (vertical or horizontal).
partGapspace.14px4pxnoVertical gap between legend, description, list, and error.
legendColorcolor.foreground #2a2b2f #ecedeeyespart legend.
legendSizefont.size.md16px16pxnopart legend.
legendWeightfont.weight.medium500500nopart legend.
labelColorcolor.foreground #2a2b2f #ecedeeyespart radioLabel.
labelSizefont.size.md16px16pxnopart radioLabel.
labelWeightfont.weight.regular400400nopart radioLabel.
helperSizefont.size.sm13px13pxnoSize of description, radioDescription and errorMessage; reaches the composed Text only through its fontSize override. Forwarded-only, so it has no —ds-radio-group-* hook on web or Lit; consumers change it through overrides.
descriptionTextcolor.foreground.muted #56585c #bcbec1yespart description: Realised by the composed Text’s tone muted (description and radioDescription); no —ds-radio-group-* hook.
errorTextcolor.foreground.danger #b51d26 #ff958dyespart errorMessage: Realised by the composed Text’s tone danger; no —ds-radio-group-* hook.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnoApplies to legend and radioLabel (their own rule on web/Lit, keeping its hook; a Text override on native) and is forwarded to the description, radioDescription and errorMessage Texts.
lineHeightfont.lineHeight.normal1.51.5noAs fontFamily: legend and radioLabel directly, forwarded to every composed Text.
focusRingcolor.border.focus #496ded #99b5ffyespart radio: On focus-visible the radio’s border becomes focusRingWidth in this color, replacing controlBorderWidth/controlBorder (web, Lit and native alike); the option row is not outlined. It does not replace controlBorderInvalid: in an invalid group a focused radio keeps the danger colour and takes only the focus width, as Checkbox does.
focusRingWidthborder.width.focus2px2pxyespart radio.
minTargetsize.target.comfortable44px44pxyesMinimum height of each option row; the whole row is the hit area, radioDescription included — a click anywhere in the row, the description among it, selects that option. It is a minimum, not the height: at the default tokens a single-line row is exactly this tall and optionPaddingBlock has no visible effect, and an override of that padding can only grow the row past this floor, never shrink it below.
disabledOpacityopacity.disabled0.50.5noDims option rows only. A disabled option in an enabled group dims its own row; a disabled group dims every option row once (not stacked with the option’s own opacity) and leaves legend, description and errorMessage at full opacity.
transitionmotion.duration.fast120ms120msnopart radio: The selected border color and the dot’s opacity (fading in and out), with motion.easing.standard, on every platform. Invalid and focus border changes switch instantly — one-directionally, which is all CSS can express from a single border-color transition: entering those states is instant, leaving them fades back over transition, and that asymmetry is accepted rather than paid for with an extra element. Instant under reduced motion.

Component doc

BindingTokenLightDarkLockedDescription
backgroundcolor.control.background #ffffff #2a2b2fyes
foregroundcolor.foreground #2a2b2f #ecedeeyes
placeholdercolor.foreground.muted #56585c #bcbec1yes
iconColorcolor.foreground.muted #56585c #bcbec1yespart icon: The leading search glyph only, forwarded to that Icon’s own color override. The clear and submit glyphs keep their ghost Buttons’ own colors; nothing is forwarded into a Button. The glyph’s size is not a binding on purpose: it is derived from size (sm at md, md at lg) so the glyph always tracks the text, and is not independently themeable.
bordercolor.border.strong #939599 #707176yes
borderFocuscolor.border.focus #496ded #99b5ffyesThe field wrapper’s border while the input itself has focus (web and Lit: :has(input:focus-visible), not :focus-within, so a focused clear or submit Button shows only its own ring). The input draws no outline of its own.
borderWidthborder.width.thin1px1pxno
radiusradius.full999px999pxnoA pill: the one place the system uses the full radius on a field, so search reads as search.
paddingInlinespace.md12px12pxno
paddingBlockspace.sm8px8pxno
paddingBlock (size=lg)space.md12px12pxno
affixGaplayout.gap.tight4px4pxnoBetween the glyph, the input and the buttons.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifno
fontSizefont.size.md16px16pxno
fontSizefont.size.lg19px19pxno
lineHeightfont.lineHeight.normal1.51.5no
suggestionsOffsetspace.14px4pxnopart suggestions: Gap between the field and the popup. It is folded into the fixed popup’s own position calculation — exactly the mechanism ds-combobox uses — not applied as a margin afterwards, which would position the popup and then shift it again.
popupSurfacecolor.overlay.surface #ffffff #2a2b2fno
popupBordercolor.border #d9dbdd #414246no
popupBorderWidthborder.width.thin1px1pxno
popupRadiusradius.md4px4pxno
popupShadowshadow.overlay0px 8px 24px 0px #0000002e0px 8px 24px 0px #00000080no
layerlayer.dropdown100100noStacking of the suggestions popup on web and Lit, placed below the field and flipped above it when the viewport has no room, as Combobox. Unused on native, where the list is inline: it stays in the native overridable set for parity and an override of it there is accepted and does nothing.
partGapspace.14px4pxnoBetween the visible label and the field.
labelWeightfont.weight.medium500500nopart label: Forwarded to the label Text’s own fontWeight override; the label’s size follows fontSize, forwarded as its fontSize override, so a shown label tracks size as Input’s does. Both forwards always carry the binding’s token (font.size.{size} when not overridden). Forwarded bindings (this, fontSize to the label, iconColor) reach the child only through its overrides; they have no —ds-search-* CSS hook, and none is defined on :host either — an unread custom property would look like a working one. They are reachable through the overrides prop and nowhere else, which is the exception to the “every binding is also a custom property” rule.
minTargetsize.target.comfortable44px44pxyes
focusRingWidthborder.width.focus2px2pxyesReplaces borderWidth on the field wrapper while the input is focused (the field border, in borderFocus, is the focus ring, as Input and Combobox); paddingInline and paddingBlock shrink by the difference so the content does not shift. No separate outline.
disabledOpacityopacity.disabled0.50.5noApplied to the label, glyph and input of a disabled Search, not to an element containing the Buttons, which receive disabled and dim once through their own style (as NumberInput’s steppers). The field frame (border, background) is not dimmed, so the dimmed text sits on an undimmed control background and falls below the a11y.contrast figures: those pairs describe the enabled state, and the disabled state relies on the WCAG 1.4.3 exemption for inactive components, made machine-visible by aria-disabled="true" on the dimmed root as well as the input (on React Native the root View beside accessibilityState.disabled, since react-native-web drops the object form).

SegmentedControl

Component doc

BindingTokenLightDarkLockedDescription
groupBackgroundcolor.background.strong #ecedee #414246yespart group.
groupPaddingspace.14px4pxnopart group.
groupRadiusradius.md4px4pxnopart group.
segmentColorcolor.foreground.muted #56585c #bcbec1yespart segment.
segmentSelectedColorcolor.foreground.strong #000000 #ffffffyespart segment.
segmentSelectedBackgroundcolor.background #ffffff #17181byespart indicator: The raised pill under the selected segment — drawn on the indicator part, never on the segment itself.
segmentShadowshadow.raised0px 1px 3px 0px #0000001a0px 1px 3px 0px #00000059nopart indicator: The pill’s shadow. The one binding here whose token resolves to an object rather than a scalar: on React Native it expands into the shadow style keys (shadowColor, shadowOffset, shadowRadius, elevation), which are spread into the pill’s style, and an override is resolved the same way.
segmentRadiusradius.sm2px2pxnopart indicator: The pill’s corners; the segment’s focus ring uses the same radius.
segmentPaddingInlinespace.md12px12pxnopart segment.
segmentPaddingBlockspace.14px4pxnopart segment: Vertical padding at size md.
segmentGaplayout.gap.tight4px4pxnopart segment: Between icon and label inside a segment; no effect with iconOnly, where a segment has one child.
segmentSpacingspace.00px0pxnopart group: Between adjacent segments, applied as the group’s gap (never a segment margin): none — the pill slides under abutting segments.
selectedWeightfont.weight.semibold600600nopart segment: The selected segment’s label; unselected use fontWeight.
paddingBlockSmspace.14px4pxnopart segment: Vertical padding at size sm; md uses segmentPaddingBlock. The two are the same token on purpose, so size changes the height only through fontSize and its line box — and on React Native, where every segment reaches size.target.comfortable, not at all: there size changes the type and the intrinsic width only, which is the intended outcome. They stay two independent hooks all the same (an sm rule reads its own --ds-segmented-control-padding-block-sm), so overriding one never moves the other.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart segment.
fontSizefont.size.sm13px13pxnopart segment.
fontSizefont.size.md16px16pxnopart segment.
fontWeightfont.weight.medium500500nopart segment.
lineHeightfont.lineHeight.normal1.51.5nopart segment.
minTargetsize.target.min24px24pxyespart segment: Each segment’s minimum, on both axes (min-inline-size and min-block-size), so a one-character label still makes a square-ish target. The one binding whose token resolves per platform: size.target.min on web and Lit, size.target.comfortable on React Native, which is touch and where every segment therefore reaches 44px. Web and Lit keep the smaller floor because no CSS query tells a touch screen from a hybrid laptop and guessing would shrink or grow the control for the wrong people.
focusRingcolor.border.focus #496ded #99b5ffyespart segment.
focusRingWidthborder.width.focus2px2pxyespart segment.
transitionmotion.duration.fast120ms120msnopart indicator: Pill movement; instant under reduced motion.
disabledOpacityopacity.disabled0.50.5nopart segment: The segment part only: a checked-but-disabled option keeps a full-opacity pill under a faded label, since the pill is the indicator part and the selection it marks is still true.

Select

Component doc

BindingTokenLightDarkLockedDescription
triggerBackgroundcolor.background #ffffff #17181byespart trigger.
triggerBordercolor.border.strong #939599 #707176yespart trigger.
triggerBorderFocuscolor.border.focus #496ded #99b5ffyespart trigger: The border color while the trigger is keyboard-focused (:focus-visible). When the field is both invalid and focused the danger color stays and only the width changes (focusRingWidth), as Input, so the error is never hidden by focus.
triggerBorderInvalidcolor.border.danger #b51d26 #ff958dnopart trigger.
triggerBorderWidthborder.width.thin1px1pxnopart trigger.
triggerRadiusradius.md4px4pxnopart trigger.
triggerPaddingInlinespace.md12px12pxnopart trigger.
triggerPaddingBlockspace.sm8px8pxnopart trigger.
triggerPaddingBlock (size=sm)space.14px4pxnopart trigger.
triggerGaplayout.gap.normal8px8pxnopart trigger: Between value and chevron.
valueColorcolor.foreground #2a2b2f #ecedeeyespart value: Realised by the composed value Text’s default tone while an option is selected.
placeholderColorcolor.foreground.muted #56585c #bcbec1yespart value: The value Text while it shows the placeholder, realised by its muted tone.
chevroncolor.foreground.muted #56585c #bcbec1yespart chevron: Forwarded as the composed Icon’s overrides.color (never its color prop, which would win over the override), always carrying this token; like the tone-realised colors it has no —ds-select-* hook.
chevronReservefont.size.sm13px13pxnopart trigger: In effect wherever a native
partGapspace.14px4pxnoBetween label, description, trigger and error, on the field group (the root wrapper, not an anatomy part — as Input).
labelWeightfont.weight.medium500500nopart label: Reaches the label only through the composed Text’s overrides (fontWeight); no —ds-select-* hook, since a hook could not reach the child without restyling it.
helperSizefont.size.sm13px13pxnoDescription and error text size, forwarded into the composed Text’s overrides (fontSize); no —ds-select-* hook, as for labelWeight.
descriptionTextcolor.foreground.muted #56585c #bcbec1yespart description: Realised by the composed Text’s muted tone.
errorTextcolor.foreground.danger #b51d26 #ff958dyespart errorMessage: Realised by the composed Text’s danger tone.
popupSurfacecolor.overlay.surface #ffffff #2a2b2fnopart popup.
popupBordercolor.border #d9dbdd #414246nopart popup.
popupBorderWidthborder.width.thin1px1pxnopart popup.
popupShadowshadow.overlay0px 8px 24px 0px #0000002e0px 8px 24px 0px #00000080nopart popup.
popupRadiusradius.md4px4pxnopart popup.
popupOffsetspace.14px4pxnopart popup: The gap between trigger and popup, on the side the popup opens (below, or above when flipped). Applied as the popup’s block margin on both sides (a position: fixed popup has no parent gap to use); only the side facing the trigger shows, and the flip test reads the resolved margin.
layerlayer.dropdown100100nopart popup.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnoLabel, value and helper Text, and forwarded into the composed Listbox’s own fontFamily override.
fontSizefont.size.sm13px13pxnoThe value text and the label, so both follow size; description and error use helperSize. Not forwarded to the Listbox: the popup does not follow size, so options keep Listbox’s own fontSize.
fontSizefont.size.md16px16pxnoThe value text and the label, so both follow size; description and error use helperSize. Not forwarded to the Listbox: the popup does not follow size, so options keep Listbox’s own fontSize.
fontWeightfont.weight.regular400400nopart value: Weight of the trigger’s value text only. Not forwarded to Listbox’s optionWeight: like fontSize, the trigger’s value type does not reach the popup, so options keep Listbox’s own weights (optionWeight, optionSelectedWeight). It exists as an override seam for composites that set their own header type — DatePicker forwards its monthTitleWeight into it.
lineHeightfont.lineHeight.normal1.51.5noLabel, value and helper Text, and forwarded into the composed Listbox’s own lineHeight override.
minTargetsize.target.comfortable44px44pxyespart trigger.
minTargetSmsize.target.min24px24pxyespart trigger: The trigger height floor at size sm. The popup is unchanged. This is the documented exception to the target-44px requirement, which applies at md: sm exists for toolbar and calendar-header pickers, where a 44px control would not fit, and it still meets the 24px floor.
focusRingWidthborder.width.focus2px2pxyespart trigger: Replaces triggerBorderWidth while the trigger is keyboard-focused (the border is the focus ring — no outline). Block and inline padding shrink by (focusRingWidth − the resolved triggerBorderWidth) so the trigger does not shift — calc(padding - (focusRingWidth - triggerBorderWidth)), the same unclamped rule Input uses, so the two fields agree and no zero literal is needed. On React Native, where Pressable focus cannot tell keyboard from touch, the width changes on any focus — and on a device it never changes at all, since Pressable receives no focus events outside react-native-web, so there is no focus ring there.
disabledOpacityopacity.disabled0.50.5noApplied to the whole field group (label, description, trigger, error), as Input.
entermotion.duration.fast120ms120msnopart popup: Popup opacity fade with motion.easing.standard — no slide, no chevron rotation, no trigger border transition. Instant under reduced motion. Opening only: closing is instant on every platform — on web because the Popover API top layer has no exit transition without transitioning display, and elsewhere so that a popup, a sheet and a Modal all disappear the same way.

SidePanel

Component doc

BindingTokenLightDarkLockedDescription
scrimcolor.overlay.scrim #00000066 #00000099nopart scrim.
surfacecolor.overlay.surface #ffffff #2a2b2fyespart surface.
shadowshadow.overlay0px 8px 24px 0px #0000002e0px 8px 24px 0px #00000080noOverlay mode only; the persistent sidebar has a border instead.
bordercolor.border #d9dbdd #414246noThe inner edge of a persistent sidebar — the edge facing the content: inline-end for side: start, inline-start for side: end.
borderWidthborder.width.thin1px1pxno
widthlayout.maxWidth.prose572px572pxnoDefault panel width on wide screens; narrow is space.20 × 3 (a link list), wide is layout.maxWidth.content. Below the prose breakpoint the panel is the viewport minus edgeGutter; on web and Lit that is 100% of the fixed containing block, written verbatim rather than as 100vw — the two differ by a classic scrollbar, and 100% is the one that keeps the panel inside the viewport. The cap applies to all three width values, so a narrow panel on a very small screen still leaves the gutter.
widthNarrowspace.2080px80pxnospace.20 × 3. The hook holds the space.20 unit and the rule multiplies it by 3, so an override replaces the unit, not the final width.
widthWidelayout.maxWidth.content960px960pxno
edgeGutterspace.1248px48pxnoThe strip of scrim left visible beside a phone-width panel, so the page is still seen and tappable to close.
insetlayout.inset.lg16px16pxnopart body: Pads the inline edges of every part: forwarded as the body Box’s paddingInline (the Box’s block padding stays zero), and read directly as the header’s and footer’s inline padding. The block edges are padded once, on the column that holds the parts (padding-block: inset), never per part, so nothing doubles with partGap; in overlay mode the safe-area inset adds to that column’s block-start and block-end padding. On web and Lit the forward travels through the Box’s --ds-box-padding-inline set to --ds-side-panel-inset in SidePanel’s stylesheet, and overrides.paddingInline is passed only when the caller overrode inset; on rn the resolved value is always passed in overrides. No hook beyond --ds-side-panel-inset.
headerGaplayout.gap.normal8px8pxnopart header: Between title and close button.
headingGapspace.00px0pxnopart heading: Forwarded to the Heading’s marginBlockEnd, turning its own margin off (as Table and DataGrid do) so the title centers against the close button; headerGap and partGap own the spacing. It travels the way inset does: on web and Lit the heading wrapper’s rule sets the Heading’s own --ds-heading-margin-block-end from --ds-side-panel-heading-gap, so the default and any override arrive by the same route and no conditional prop is needed; on rn the resolved value is passed in overrides.
partGaplayout.gap.loose16px16pxnopart focusScope: The only space between header, body and footer: the gap of the column that holds them, which also carries padding-block from inset. On web and Lit that column is the focusScope part — FocusScope writes its own data-part="scope", so it is an overlay-owned element directly inside FocusScope (and inside the Landmark when non-modal) carrying data-part="focusScope". In modal mode the surface sits between the and FocusScope (it is the edge-positioned, width-capped, sliding box), so both modes end surface > FocusScope > column. On React Native FocusScope’s wrapper View cannot be styled or height-constrained, so FocusScope wraps the Animated surface from outside and the column is a flex-filling View inside the surface holding header, body and footer (in overlay mode inside a SafeAreaView, which ignores its own padding, so the column carries the gap and padding-block); FocusScope itself takes no testID, and the column carries testID="SidePanel.focusScope" — it is the View that carries this binding. Persistent mode renders no FocusScope at all, and there the sidebar root View is that column and carries the same testID, so the part is addressable in both modes.
footerGaplayout.gap.tight4px4pxnopart footer.
layerlayer.sheet200200noHas no effect inside the browser top layer (the modal ) or a native Modal window; it applies to the non-modal position: fixed panel and scrim and to the rn anchor view. The non-modal scrim and surface share this one layer (no token arithmetic); the scrim precedes the surface in the DOM, so the surface paints above it.
entermotion.duration.base200ms200msnoSlide in from the edge with the scrim fading over the same duration and easing; motion.easing.standard; instant under reduced motion. The easing is read from the token, not a binding, and is not overridable.
exitmotion.duration.fast120ms120msnoSlide out, and the scrim fades out, with motion.easing.exit (read from the token, not overridable). A swipe dismiss (React Native) has no momentum or decay physics: the surface holds the released offset until the consumer’s next render, and closing plays this same timing transition from that offset; if open is still true on that render it springs back. open is optional here, and an uncontrolled panel flips its own state in the same batch as the release, so the held offset is always followed by the exit and the spring-back branch belongs to controlled panels alone. A swipe released below the dismiss threshold, or a dismiss the consumer did not honor, springs back over this duration with motion.easing.standard (a timing animation, not a spring), instant under reduced motion; an interrupted enter animation is finished by the spring-back.
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyes

Slider

Component doc

BindingTokenLightDarkLockedDescription
trackcolor.background.strong #ecedee #414246nopart track.
fillcolor.control.selectedBackground #3553d2 #496dedyespart fill.
trackHeightspace.14px4pxnopart track.
trackRadiusradius.full999px999pxnopart track.
thumbcolor.control.background #ffffff #2a2b2fnopart thumb.
thumbBordercolor.control.selectedBackground #3553d2 #496dedyespart thumb.
thumbBorderWidthborder.width.focus2px2pxyespart thumb.
thumbSizespace.520px20pxnopart thumb.
thumbShadowshadow.raised0px 1px 3px 0px #0000001a0px 1px 3px 0px #00000059nopart thumb.
thumbActiveScaleopacity.disabled0.50.5nopart thumb: Not a scale — the pressed thumb shows a circular halo of the fill color at this opacity, centred on the knob, with diameter thumbSize + 2 × haloSpread. No literal scale factor exists. Focus alone never shows it. On React Native, where there is no hover and no press state distinct from a drag, the halo shows while a gesture owns that thumb (including a track press that grabbed it).
haloSpreadspace.28px8pxnopart thumb: How far the pressed-thumb halo extends beyond the knob on each side.
markcolor.border.strong #939599 #707176nopart tickMarks: Mark dot colour.
markSizespace.28px8pxnopart tickMarks: Mark dot diameter. Deliberately larger than trackHeight so a mark reads as a tick standing proud of the rail rather than a bump on it.
markLabelColorcolor.foreground.muted #56585c #bcbec1yespart tickMarks: Realised by the mark label Text’s tone muted. Locked, it keeps its --ds-slider-mark-label-color hook on web and Lit, as every locked binding does; the tick marks pass it to the mark label Text through the colour that Text resolves (its own --ds-text-color, or a re-scoped --color-foreground-muted), never by restyling the Text.
markLabelSizefont.size.xs11px11pxnopart tickMarks: Forwarded to each mark label Text’s fontSize override (fontFamily is forwarded too); never styles the Text directly.
markLabelGapspace.14px4pxnopart tickMarks: Gap between the bottom of the track area (after trackPaddingBlock) and the mark label row; partGap does not apply between them (on React Native, a column wraps the track area and the label row with this gap). The slider grows by the label line (markLabelSize × font.lineHeight.normal) only when some mark has a label.
valueColorcolor.foreground #2a2b2f #ecedeeyespart valueText: Realised by the value Text’s tone default; no hook of its own.
valueSizefont.size.sm13px13pxnopart valueText: Forwarded to the value Text’s fontSize override, and to the bubble’s Text the same way.
bubbleSurfacecolor.inverse.surface #17181b #ecedeeyespart bubble: The hover/drag value bubble uses the inverse surface, like Tooltip.
bubbleTextcolor.inverse.foreground #f6f7f8 #17181byespart bubble: The bubble’s value is a Text (size sm, tone default) with valueSize forwarded. Its colour is not forwarded (Text color is locked): web and Lit re-scope --color-foreground on the bubble, React Native provides TextForegroundContext, as Text’s color binding describes.
bubblePaddingBlockspace.14px4pxnopart bubble.
bubblePaddingInlinespace.28px8pxnopart bubble.
bubbleOffsetspace.14px4pxnopart bubble: Gap between the bubble’s bottom edge and the top of the active thumb’s hit area (minTarget); the bubble is centred horizontally on the thumb.
bubbleRadiusradius.sm2px2pxnopart bubble: The bubble’s corners, the same shape as a Tooltip. The bubble is its own part (not the valueText Text): an inverse-surface pill above the active thumb.
labelWeightfont.weight.medium500500nopart label: Forwarded to the label Text’s fontWeight override.
partGapspace.14px4pxnoVertical gap on the slider root between the label row, the track area, and the description/error message.
labelGapspace.28px8pxnopart label: Horizontal gap between the label and the value text in the label row.
trackPaddingBlockspace.312px12pxnopart track: Vertical space around the track so the thumb and its halo have room and the touch target reaches the comfortable size. It pads the track area, an unparted wrapper around the track and thumbs that is also the pointer hit area, never the coloured rail itself (padding the rail would thicken it).
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart label: Forwarded to every composed Text’s fontFamily override (label, value, bubble, mark labels, description, error); never styles them directly.
fontSizefont.size.md16px16pxnopart label: Forwarded to the label Text’s fontSize override.
helperSizefont.size.sm13px13pxnopart description: Forwarded to the description and error message Texts’ fontSize overrides.
descriptionTextcolor.foreground.muted #56585c #bcbec1yespart description: Realised by the composed Text’s muted tone; no hook of its own.
errorTextcolor.foreground.danger #b51d26 #ff958dyespart errorMessage: Realised by the composed Text’s danger tone; no hook of its own, since Text’s colour is locked and a hook could not reach the child without restyling it (the same as Input). Locked means absent from the overridable type, not accepted and ignored.
minTargetsize.target.comfortable44px44pxyespart thumb: The thumb’s hit area, centred on the knob. It is taller than the track area (trackHeight + 2 × trackPaddingBlock) and is allowed to overflow it above and below rather than growing the row: nothing clips it, and the label row and mark label row paint above the thumbs and take any press that lands on them.
focusRingcolor.border.focus #496ded #99b5ffyespart thumb: A circular ring around the visible knob (not the minTarget hit area), shown only while the thumb has keyboard focus.
focusRingWidthborder.width.focus2px2pxyespart thumb: Ring thickness, drawn outside the knob’s border and offset from it by the same width.
disabledOpacityopacity.disabled0.50.5noApplied to the slider root (label row, track area, marks and messages) while disabled. The element that dims also carries aria-disabled="true" — the root as well as each thumb (on Lit the shadow-root wrapper, on React Native the root View beside accessibilityState.disabled) — so a contrast checker resolves the dimmed label and value text to an inactive component and applies the WCAG 1.4.3 exemption instead of reporting a failure. This holds for every component whose disabledOpacity dims text.
transitionmotion.duration.fast120ms120msnoHalo (part thumb) and bubble (part bubble) appearance; the thumb itself follows the pointer with no transition.

Splitter

Component doc

BindingTokenLightDarkLockedDescription
separatorSizespace.14px4pxnopart separator: The visible line. The grab area is wider: handleSize centered on it.
separatorColorcolor.border #d9dbdd #414246nopart separator.
separatorHovercolor.border.strong #939599 #707176yespart separator, state hover: Pointer hover; unused on native.
separatorActivecolor.control.selectedBackground #3553d2 #496dedyespart separator, state dragging: While dragging or focused (:focus-visible). The dragging state is a ds-splitter--dragging root class on web and a data-dragging attribute on the Lit separator. On native the separator has no focus events, so it applies only while dragging.
handleSizespace.312px12pxnopart handle: Pointer grab area, centered on the separator and overlapping both panes so the panes’ content keeps the full width. The effective hit area is max(handleSize, minTarget), so the locked floor cannot be overridden away. handle is a pseudo-element part on web and Lit — the separator’s ::before — so nothing there matches a part selector for it; on native it is the absolutely positioned child View, which carries the testID.
gripcolor.border.strong #939599 #707176yesA short centered grip mark on the separator, so the divider reads as draggable: a rounded bar of gripLength along the separator and separatorSize across it. The grip is a mark inside the separator and not an anatomy part of its own — a pseudo-element on web and Lit, a child View on native — which is why it, gripLength and gripRadius name no part.
gripLengthspace.624px24pxno
gripRadiusradius.full999px999pxnoRounds the grip bar’s ends.
collapseButtonOffsetspace.28px8pxnopart collapseButton: Distance of the collapse Button from the separator’s start edge along the separator (top of a vertical separator, inline-start of a horizontal one); the button is centered across the separator and overlaps both panes. Expanded, that centering is the anchor: the button’s cross-axis center sits on the separator line. While collapsed the primary pane has no size, so the button aligns to the secondary pane’s start edge instead of hanging outside the container — its cross-axis start edge sits at the separator’s end edge, with no centering, so no part of it falls outside.
paneMinTargetsize.target.comfortable44px44pxyesA pane never shrinks below this on the drag axis before collapsing, so its scrollbar and content stay usable: a CSS minmax() floor on both grid tracks beneath the percent clamp (a very narrow container can therefore show the pane wider than its percent). Dropped for the primary track while collapsed so collapse reaches zero. On native it is minWidth (or minHeight when vertical) on both panes, dropped for the primary pane while collapsed. A React Native floor is hard, not a preference: the primary pane takes flexShrink: 0, so where the container is too narrow for both floors plus the separator the secondary shrinks to its own floor and then the splitter overflows, rather than either pane dropping below its floor as the web grid allows.
minTargetsize.target.min24px24pxyesSeparator hit area along the drag axis.
focusRingcolor.border.focus #496ded #99b5ffyesThe separator’s ring on web and Lit. The native separator is a plain View with a PanResponder and has no focus events, so on React Native this reaches only the composed collapse Button — as separatorHover has no native expression either.
focusRingWidthborder.width.focus2px2pxyesWidth of that ring, on the same elements.
transitionmotion.duration.fast120ms120msnoCollapse and restore, and the separator color; dragging itself has no transition. The pane size is a custom property driving a grid track, which only animates where @property registration is reliable — where it is not, collapse and restore are instant and only the separator colour transitions. Only a change of the collapsed state animates; key steps and drags resize instantly. Web and Lit register the same property the same way (--ds-splitter-primary-size, syntax <percentage>, not inherited): Lit calls CSS.registerProperty at module load in try/catch because @property does not apply inside shadow roots. The animating state gets a named hook of its own, as the dragging state does: a ds-splitter--animate root class on web and a data-animating attribute on the Lit container, set when the collapsed state changes and cleared by the next size change or the next pointerdown — a collapse that is never followed by a resize leaves it set, which is what lets a second toggle animate too. On native, collapse and restore animate an Animated.Value interpolated to the primary pane’s flexBasis percentage (no native driver, since react-native-web has none) and the separator colour switches instantly.

Stack

Component doc

BindingTokenLightDarkLockedDescription
gaplayout.gap.none0px0pxnogap: none resolves layout.gap.none, a real token that is zero, and makes overrides.gap a no-op, per the presence rule; on web and Lit the none rule reads var(--layout-gap-none) directly rather than the --ds-stack-gap hook, so consumer CSS on the hook is ignored there too. It is read as a token on every platform — none of them writes a bare 0.
gaplayout.gap.tight4px4pxnogap: none resolves layout.gap.none, a real token that is zero, and makes overrides.gap a no-op, per the presence rule; on web and Lit the none rule reads var(--layout-gap-none) directly rather than the --ds-stack-gap hook, so consumer CSS on the hook is ignored there too. It is read as a token on every platform — none of them writes a bare 0.
gaplayout.gap.normal8px8pxnogap: none resolves layout.gap.none, a real token that is zero, and makes overrides.gap a no-op, per the presence rule; on web and Lit the none rule reads var(--layout-gap-none) directly rather than the --ds-stack-gap hook, so consumer CSS on the hook is ignored there too. It is read as a token on every platform — none of them writes a bare 0.
gaplayout.gap.loose16px16pxnogap: none resolves layout.gap.none, a real token that is zero, and makes overrides.gap a no-op, per the presence rule; on web and Lit the none rule reads var(--layout-gap-none) directly rather than the --ds-stack-gap hook, so consumer CSS on the hook is ignored there too. It is read as a token on every platform — none of them writes a bare 0.
gaplayout.gap.section32px32pxnogap: none resolves layout.gap.none, a real token that is zero, and makes overrides.gap a no-op, per the presence rule; on web and Lit the none rule reads var(--layout-gap-none) directly rather than the --ds-stack-gap hook, so consumer CSS on the hook is ignored there too. It is read as a token on every platform — none of them writes a bare 0.

Stepper

Component doc

BindingTokenLightDarkLockedDescription
indicatorSizespace.624px24pxnopart indicator.
indicatorColorcolor.foreground #2a2b2f #ecedeeyespart indicator: The numeral on an upcoming or current step; the complete and error states have their own foregrounds.
indicatorBackgroundcolor.control.background #ffffff #2a2b2fnopart indicator.
indicatorBordercolor.border.strong #939599 #707176yespart indicator.
indicatorBorderWidthborder.width.focus2px2pxyespart indicator.
indicatorCompleteBackgroundcolor.control.selectedBackground #3553d2 #496dedyespart indicator.
indicatorCompleteForegroundcolor.control.selectedForeground #ffffff #000000yespart indicator: The check Icon on a complete step, passed as the Icon’s color override.
indicatorCompleteBordercolor.control.selectedBackground #3553d2 #496dedyespart indicator: The ring of a complete indicator, the same colour as its fill.
indicatorRadiusradius.full999px999pxnopart indicator: Makes the indicator a circle.
indicatorCurrentBordercolor.control.selectedBackground #3553d2 #496dedyespart indicator: The ring of a step whose resolved status is current; an error on the id-matched step shows the error ring instead.
indicatorErrorBackgroundcolor.status.danger.background #fff2f0 #4c0005yespart indicator.
indicatorErrorForegroundcolor.status.danger.foreground #b51d26 #ffc3bdyespart indicator: The danger Icon on an error step, passed as the Icon’s color override, as indicatorCompleteForeground is for the check.
indicatorErrorBordercolor.status.danger.icon #b51d26 #ff958dyespart indicator: The ring; the danger icon step is the one guaranteed 3:1 against the page.
indicatorFontSizefont.size.sm13px13pxnopart indicator: The numeral. The check and danger Icons take size="sm" and receive this as their size override, so glyph and numeral match. A font-size token driving an icon box is intended here (both resolve to a length, and the point is that the glyph tracks the numeral); because an override always wins over the prop, size="sm" on those two Icons is decorative.
indicatorFontWeightfont.weight.semibold600600nopart indicator.
connectorcolor.border #d9dbdd #414246nopart connector.
connectorCompletecolor.control.selectedBackground #3553d2 #496dedyespart connector: The connector after step i when step i is before the current step by position; explicit statuses (error included) do not change it. Every other connector uses connector.
connectorWidthborder.width.focus2px2pxyespart connector.
labelColorcolor.foreground #2a2b2f #ecedeeyespart label: Realised by the label Text’s tone default on every step that is not upcoming; no —ds-stepper-* hook.
labelUpcomingColorcolor.foreground.muted #56585c #bcbec1yespart label: Realised by the label Text’s tone muted on a step whose resolved status is upcoming (an explicit status: upcoming is muted, an explicit current after the id match is not); no —ds-stepper-* hook. The label’s tone is passed per step, alongside the listed props.
labelWeightfont.weight.medium500500nopart label: Forwarded to the label Text’s fontWeight override on every step but the current one.
labelCurrentWeightfont.weight.semibold600600nopart label: Forwarded to the label Text’s fontWeight override on the current step (the id match).
labelSizefont.size.sm13px13pxnopart label: Forwarded to the label Text’s fontSize override.
descriptionColorcolor.foreground.muted #56585c #bcbec1yespart description: Realised by the description Text’s tone muted; no —ds-stepper-* hook.
descriptionSizefont.size.xs11px11pxnopart description: Forwarded to the description Text’s fontSize override.
countColorcolor.foreground.muted #56585c #bcbec1yespart count: Realised by the count Text’s tone muted; no —ds-stepper-* hook.
countSizefont.size.sm13px13pxnopart count: Forwarded to the count Text’s fontSize override.
stepHovercolor.action.ghost.backgroundHover #f6f7f8 #2a2b2fnopart step, state hover: Hover and press background of a navigable step (on React Native while the Pressable is pressed or hovered).
stepRadiusradius.sm2px2pxnopart step.
stepPaddingspace.28px8pxnopart step: Inline padding of a step control (block padding too when vertical), so the hover background has room around the indicator and label. The step part is the list item (web/Lit <li>); stepHover, stepRadius, stepPadding, minTarget and the focus ring style the control inside it.
stepGaplayout.gap.normal8px8pxnopart step: Between steps along the orientation axis (the connector fills it). Horizontally the connector grows with free inline space from a minimum of stepGap; vertically there is no free space in a column of auto-height steps, so a vertical connector is exactly stepGap tall. It is centred on the indicator’s axis, and the offset takes stepPadding only on the axis the padding is on: horizontally calc((indicatorSize − connectorWidth) / 2) as the block offset, vertically calc(stepPadding + (indicatorSize − connectorWidth) / 2) as the inline offset. It does not extend into stepPadding along the orientation axis, so the line stops short of the neighbouring indicators. Where the calc has no equivalent (React Native), the connector track is made exactly indicatorSize on the cross axis and inset by the focus border instead, which lands on the same centre line.
partGapspace.28px8pxnopart step: Inside the step control, between the indicator and its label (and between label and description). Not the control’s padding; that is stepPadding.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart list: Set on the root and forwarded to each composed Text’s fontFamily override. On React Native a View carries no text style and nothing inherits, so there it is set on the indicator numeral — the only text Stepper draws itself — and reaches every other string through the forwards.
minTargetsize.target.min24px24pxyespart step: Minimum block and inline size of a navigable step control. Scoped to the navigable control, as focusRing and stepHover are: a display-only step is not a target and carries no floor. Both kinds of step still carry the transparent focusRingWidth border, so navigable and display-only steps occupy the same box and stay aligned in one list.
focusRingcolor.border.focus #496ded #99b5ffyespart step: Focus-visible ring on a navigable step control. Drawn outside the control on web and Lit; on React Native, which has no outline, it is a border inside the box, so every step control reserves focusRingWidth as a transparent border and the connector geometry compensates for it.
focusRingWidthborder.width.focus2px2pxyespart step: Width of the focus-visible ring on a navigable step control.
transitionmotion.duration.fast120ms120msnopart connector.

Switch

Component doc

BindingTokenLightDarkLockedDescription
trackOffcolor.control.trackOff #939599 #707176yespart track.
trackOncolor.control.selectedBackground #3553d2 #496dedyespart track.
thumbcolor.control.selectedForeground #ffffff #000000yespart thumb: Thumb color in both states. Web and Lit draw the thumb the same way: a Switch-owned aria-hidden with pointer-events: none, stacked over the input in a track wrapper span (as Checkbox’s indicator), carrying data-part="thumb" (web) / part="thumb" and data-part="thumb" (Lit). Not a ::before of the input: Firefox draws no pseudo-element on an appearance: none input. Its state styles key off the input’s [aria-checked='true'] (the component state, so controlled mode looks right), never :checked. RN: no view (the native Switch draws it), so there is no Switch.thumb testID. SwiftUI: a shape in the ToggleStyle.
trackWidthspace.1040px40pxnopart track: Overridable on web, Lit and SwiftUI. RN: drawn by the native Switch, so it is not in the RN overridable binding type.
trackHeightspace.624px24pxnopart track: As trackWidth: not overridable on RN.
thumbSizespace.520px20pxnopart thumb: Thumb diameter; it travels trackWidth − thumbSize − 2 × thumbInset. Not overridable on RN.
thumbInsetspace.14px4pxnopart thumb: The inline gap between the thumb and the track edge, at each end. On the block axis the inset is derived from the geometry, (trackHeight − thumbSize) / 2, not taken from this binding: the thumb is always centred across the track, so an override of thumbInset or trackHeight moves the ends without ever pushing the thumb off centre. At the default tokens the two happen to agree. Not overridable on RN.
radiusradius.full999px999pxnopart track: Applies to both the track and the thumb. Not overridable on RN.
gapspace.312px12pxnopart label: Gap between track and label (the row’s flex gap). The gap is part of the hit area: a click or press on it toggles.
partGapspace.14px4pxnopart description: Vertical gap between label and description (the text column).
labelColorcolor.foreground #2a2b2f #ecedeeyespart label.
labelSizefont.size.md16px16pxnopart label: Also sets the track alignment: the track sits in a slot one label line tall (labelSize × lineHeight), aligned with the top of the text column and centred on the label’s first line. The row centres its content (track slot plus text column) on the cross axis, so a one-line row is vertically centred in minTarget; a wrapping label or a description grows the content and the track stays on the first line. RN: the native Switch (about 31pt on iOS) can be taller than the line slot; it is centred on the slot and overflows it evenly above and below, and the row’s minHeight still contains it.
labelWeightfont.weight.regular400400nopart label.
helperSizefont.size.sm13px13pxnopart description: Description text size. Reaches the composed Text only through its fontSize override, with fontFamily and lineHeight forwarded the same way; no —ds-switch-* hook. Rule for every binding that is only forwarded to a composed child (helperSize, descriptionText): it gets no —ds-switch-* CSS hook on web or Lit, so it is changed through the Switch overrides property only, never from CSS.
descriptionTextcolor.foreground.muted #56585c #bcbec1yespart description: Realised by the composed Text’s muted tone; no —ds-switch-* hook.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart label: The label’s own rule, and forwarded to the description Text.
lineHeightfont.lineHeight.normal1.51.5nopart label: As fontFamily: the label’s own rule, forwarded to the description Text.
focusRingcolor.border.focus #496ded #99b5ffyespart track: Web/Lit/SwiftUI only. RN: the native Switch has no focus events, so the OS focus indicator is used and this binding is not applied.
focusRingWidthborder.width.focus2px2pxyespart track: Drawn around the track, offset by focusRingWidth like every other control. Not applied on RN (OS indicator).
minTargetsize.target.comfortable44px44pxyespart label: Minimum block size of the full-width root (web/Lit: min-block-size; RN: the root’s minHeight). It is carried by the root, which is a column box that centres a single inner content row — an unnamed box with no part and no hook — and that inner row is what top-aligns the track slot against the text column. One box cannot both centre itself in minTarget and top-align its items, and Switch takes the two-box route rather than Checkbox’s symmetric padding, which this component’s rows forbid. The root has no vertical padding, and the whole root toggles: the empty band above and below the content row inside minTarget is hit area too, as is the gap between label and track.
disabledOpacityopacity.disabled0.50.5nopart track: Dims the track (with its thumb), the label and the description. Web/Lit: on the track wrapper and on the text column wrapper that holds the label and the description, never on the composed description Text itself. RN: on the whole Pressable row (track, label and description), unlike Checkbox, which leaves its description undimmed.
transitionmotion.duration.fast120ms120msnopart thumb: Thumb travel and track color, with motion.easing.standard. Under reduced motion both are removed: the thumb jumps and the color changes instantly. Not applied on RN (OS animation).

Table

Component doc

BindingTokenLightDarkLockedDescription
surfacecolor.background #ffffff #17181byes
headerSurfacecolor.background.subtle #f6f7f8 #2a2b2fyespart header.
headerColorcolor.foreground #2a2b2f #ecedeeyespart header.
headerWeightfont.weight.semibold600600nopart header.
headerSizefont.size.sm13px13pxnopart header.
headerBordercolor.border.strong #939599 #707176nopart header.
headerBorderWidthborder.width.thin1px1pxnopart header.
headerShadowshadow.raised0px 1px 3px 0px #0000001a0px 1px 3px 0px #00000059nopart header: Shown under the sticky header only once the body has scrolled beneath it.
rowBordercolor.border #d9dbdd #414246nopart row.
rowBorderWidthborder.width.thin1px1pxnopart row.
rowStripecolor.background.subtle #f6f7f8 #2a2b2fyespart row.
rowHovercolor.action.ghost.backgroundHover #f6f7f8 #2a2b2fnopart row, state hover: Interactive rows only (onRowPress or a Link in the row header). Hover never appears on plain rows. A render function’s output cannot be inspected, so web and Lit find the link with a :has() selector on the row-header cell — hover alone, with neither the pointer cursor nor the row press that onRowPress rows get — and React Native, which has no such selector, tints only onRowPress rows.
rowSelectedcolor.background.subtle #f6f7f8 #2a2b2fyespart row: Same tint as a stripe; the start-edge bar and the checkbox are what say selected.
rowSelectedBordercolor.control.selectedBackground #3553d2 #496dedyespart row: A start-edge bar on selected rows, so selection is not color-fill alone. It never shifts content: web and Lit draw it as an inset box-shadow on the row’s first cell (on the row itself when stacked), mirrored under RTL; React Native always reserves a start border of rowSelectedBorderWidth, coloured as the row background on an unselected row in the column layout and as rowBorder on an unselected stacked block, so the block’s outline stays unbroken.
rowSelectedBorderWidthborder.width.focus2px2pxyespart row.
cellColorcolor.foreground #2a2b2f #ecedeeyespart cell.
cellMutedColorcolor.foreground.muted #56585c #bcbec1yespart cell: Secondary values (a date beside a title) rendered with Text tone muted.
cellPaddingInlinelayout.inset.md12px12pxnopart cell.
cellPaddingInline (density=compact)layout.inset.sm8px8pxnopart cell.
cellPaddingBlockspace.sm8px8pxnopart cell.
cellGaplayout.gap.tight4px4pxnopart cell: Between a sort button’s label and its arrow, and between actions in the actions cell.
captionSizefont.size.md16px16pxnopart caption.
captionWeightfont.weight.semibold600600nopart caption.
captionGapspace.28px8pxnopart caption: Between caption and header.
stackedRowInsetlayout.inset.md12px12pxno
stackedRowGaplayout.gap.tight4px4pxnoBetween label/value pairs inside a stacked row.
stackedBlockGaplayout.gap.tight4px4pxnoBetween stacked row blocks: the body’s gap (web and Lit: the stacked tbody grid; React Native: the list’s contentContainerStyle gap). Each block is outlined with rowBorder/rowBorderWidth on stackedRowRadius.
stackedLabelColorcolor.foreground.muted #56585c #bcbec1yespart stackedLabel.
stackedLabelSizefont.size.xs11px11pxnopart stackedLabel.
stackedLabelWeightfont.weight.medium500500nopart stackedLabel.
stackedRowRadiusradius.md4px4pxno
stickyColumnShadowshadow.raised0px 1px 3px 0px #0000001a0px 1px 3px 0px #00000059noresponsive: scroll: the sticky row-header column casts this once scrolled.
scrollFadespace.624px24pxnoEdge fade width on the scroll region; an edge fades only while columns are hidden past it, measured as the scroll offset against scrollWidth − clientWidth with a 1px tolerance and re-measured on scroll and on size changes, with the mask flipped under RTL. The mask covers the scrolling content only, never the region’s own focus ring, which stays whole at a faded edge: a mask clips everything its element paints, so on web and Lit the ring is drawn by an unnamed wrapper around the scroll region (:has() on the region’s :focus-visible) while the region itself sets outline: none. That wrapper is not an anatomy part and exists in this mode alone. React Native draws it with react-native-svg, as Toolbar does.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifno
fontSizefont.size.sm13px13pxno
lineHeightfont.lineHeight.normal1.51.5no
numericFontfont.family.monoui-monospace, SFMono-Regular, Menlo, Consolas, monospaceui-monospace, SFMono-Regular, Menlo, Consolas, monospacenoBody cells of columns with align end always use this family plus tabular figures (font-variant-numeric: tabular-nums on web and Lit) — no platform can detect whether the body font has tabular figures, so there is no fallback test.
minTargetsize.target.min24px24pxyes
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyes
transitionmotion.duration.fast120ms120msnoThe interactive row’s hover background only; the sort arrow is a swapped Icon inside the Button and changes instantly, as does sorting itself.

Tabs

Component doc

BindingTokenLightDarkLockedDescription
tabColorcolor.foreground.muted #56585c #bcbec1yespart tab.
tabSelectedColorcolor.foreground.strong #000000 #ffffffyespart tab.
tabHoverBackgroundcolor.background.subtle #f6f7f8 #2a2b2fyespart tab, state hover.
tabPaddingBlockspace.sm8px8pxnopart tab.
tabPaddingInlinespace.md12px12pxnopart tab.
tabGaplayout.gap.tight4px4pxnopart tab: Between icon, label and badge inside a tab.
listGaplayout.gap.none0px0pxnopart tablist: Tabs touch; the indicator separates them.
indicatorcolor.control.selectedBackground #3553d2 #496dedyespart indicator: The selected tab’s underline (horizontal, flush against the list border at the bottom edge) or side bar (vertical, flush against the inline-end edge next to the panels). It sits inside the list at inset 0 of that edge, directly against the border rather than over it, because the scrolling list clips anything outside its padding box. It is placed instantly on first render and when the selected tab first appears, and animates only when the selection moves between tabs: the component gates its own transition (web: a data-animate attribute on the indicator part, an implementation hook rather than a declared attribute; React Native: a has-measured latch, so the first measured placement snaps), and a resize remeasure never animates. It is positioned from the selected tab’s measured offset within the list, which is a requirement stated in measurements and not in a formula — each platform computes it in the direction it can (web from the list’s inline-start edge, so RTL is measured from the right; Lit anchors at the start edge and translates; React Native branches on I18nManager.isRTL, since its position and border props are physical). With a selection that matches no tab — a value with no entry, or an empty tabs array — it renders at zero size rather than staying on the last tab. The colour is the selected-control fill, which is chosen per mode to meet 3:1 on the page (the primary button fill is not).
indicatorThicknessborder.width.focus2px2pxyespart indicator.
listBordercolor.border #d9dbdd #414246nopart tablist: The rule under the whole tab list (horizontal), or along its inline-end edge (vertical).
listBorderWidthborder.width.thin1px1pxnopart tablist.
panelGaplayout.gap.loose16px16pxnopart panel: Between the tab list and the panel. Applied as the gap of the root flex layout that holds the list and the panels (the host on Lit), not as a margin on each panel; part: panel names what the gap sits before, not the element that carries it.
badgeColorcolor.foreground.muted #56585c #bcbec1yespart tabBadge.
badgeWeightfont.weight.regular400400nopart tabBadge: The badge’s font weight; lighter than the label’s fontWeight so the count reads as secondary.
badgeSizefont.size.xs11px11pxnopart tabBadge: The badge’s font size; its line height is the same lineHeight multiplier as the label, applied to this size. That works because font.lineHeight.normal is unitless in every theme — a themed value with units would become a fixed length here.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnopart tab.
fontSizefont.size.md16px16pxnopart tab.
fontWeightfont.weight.medium500500nopart tab.
lineHeightfont.lineHeight.normal1.51.5nopart tab.
radiusradius.sm2px2pxnopart tab: On the tab’s hover background and focus ring.
minTargetsize.target.comfortable44px44pxyespart tab: Minimum block size and minimum inline size of every tab (both axes).
focusRingcolor.border.focus #496ded #99b5ffyespart tab: Also the focusable panel’s ring: the panel has no binding of its own and draws from these same two hooks.
focusRingWidthborder.width.focus2px2pxyespart tab.
transitionmotion.duration.fast120ms120msnopart indicator: Indicator movement, with motion.easing.standard; instant under reduced motion.
disabledOpacityopacity.disabled0.50.5nopart tab.

Text

Component doc

BindingTokenLightDarkLockedDescription
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifno
fontSizefont.size.xs11px11pxno
fontSizefont.size.sm13px13pxno
fontSizefont.size.md16px16pxno
fontSizefont.size.lg19px19pxno
fontSizefont.size.xl23px23pxno
fontWeightfont.weight.regular400400no
fontWeightfont.weight.medium500500no
fontWeightfont.weight.semibold600600no
fontWeightfont.weight.bold700700no
lineHeightfont.lineHeight.normal1.51.5no
colorcolor.foreground #2a2b2f #ecedeeyesLocked, and locked means absent from the overridable type — so a composing parent that forwards { color: … } gets a compile error, not a silent no-op. A surface with its own foreground (Toast, Tooltip, Slider’s value bubble on color.inverse.foreground) therefore does not recolour Text at all: on web and Lit it re-scopes --color-foreground on its own container, which the tone resolves through and which touches no part of the child; on React Native, where there is no cascade, it provides the package-internal TextForegroundContext, which Text reads only while tone is default. onAction is not a stand-in for either — in dark mode it is near-white while the inverse foreground is near-black. A control that paints its own selected text (a DatePicker day) draws that text itself rather than asking Text for a colour it has no tone for. Locked, it still keeps its --ds-text-color hook on web and Lit, as every locked binding does, and the tone rule reads the hook. Each tone sets the hook’s default to the token’s own custom property — default is the bare var(--color-foreground) an inverse surface re-scopes, and onAction is var(--color-foreground-on-action) (camelCase to kebab-case) — so re-scoping --color-foreground on a container still recolours the default tone. TextForegroundContext exists on React Native only: it is exported from the rn Text.tsx for sibling components and not re-exported from the package index. The web Text.tsx has no such export.
colorcolor.foreground.strong #000000 #ffffffyesLocked, and locked means absent from the overridable type — so a composing parent that forwards { color: … } gets a compile error, not a silent no-op. A surface with its own foreground (Toast, Tooltip, Slider’s value bubble on color.inverse.foreground) therefore does not recolour Text at all: on web and Lit it re-scopes --color-foreground on its own container, which the tone resolves through and which touches no part of the child; on React Native, where there is no cascade, it provides the package-internal TextForegroundContext, which Text reads only while tone is default. onAction is not a stand-in for either — in dark mode it is near-white while the inverse foreground is near-black. A control that paints its own selected text (a DatePicker day) draws that text itself rather than asking Text for a colour it has no tone for. Locked, it still keeps its --ds-text-color hook on web and Lit, as every locked binding does, and the tone rule reads the hook. Each tone sets the hook’s default to the token’s own custom property — default is the bare var(--color-foreground) an inverse surface re-scopes, and onAction is var(--color-foreground-on-action) (camelCase to kebab-case) — so re-scoping --color-foreground on a container still recolours the default tone. TextForegroundContext exists on React Native only: it is exported from the rn Text.tsx for sibling components and not re-exported from the package index. The web Text.tsx has no such export.
colorcolor.foreground.muted #56585c #bcbec1yesLocked, and locked means absent from the overridable type — so a composing parent that forwards { color: … } gets a compile error, not a silent no-op. A surface with its own foreground (Toast, Tooltip, Slider’s value bubble on color.inverse.foreground) therefore does not recolour Text at all: on web and Lit it re-scopes --color-foreground on its own container, which the tone resolves through and which touches no part of the child; on React Native, where there is no cascade, it provides the package-internal TextForegroundContext, which Text reads only while tone is default. onAction is not a stand-in for either — in dark mode it is near-white while the inverse foreground is near-black. A control that paints its own selected text (a DatePicker day) draws that text itself rather than asking Text for a colour it has no tone for. Locked, it still keeps its --ds-text-color hook on web and Lit, as every locked binding does, and the tone rule reads the hook. Each tone sets the hook’s default to the token’s own custom property — default is the bare var(--color-foreground) an inverse surface re-scopes, and onAction is var(--color-foreground-on-action) (camelCase to kebab-case) — so re-scoping --color-foreground on a container still recolours the default tone. TextForegroundContext exists on React Native only: it is exported from the rn Text.tsx for sibling components and not re-exported from the package index. The web Text.tsx has no such export.
colorcolor.foreground.danger #b51d26 #ff958dyesLocked, and locked means absent from the overridable type — so a composing parent that forwards { color: … } gets a compile error, not a silent no-op. A surface with its own foreground (Toast, Tooltip, Slider’s value bubble on color.inverse.foreground) therefore does not recolour Text at all: on web and Lit it re-scopes --color-foreground on its own container, which the tone resolves through and which touches no part of the child; on React Native, where there is no cascade, it provides the package-internal TextForegroundContext, which Text reads only while tone is default. onAction is not a stand-in for either — in dark mode it is near-white while the inverse foreground is near-black. A control that paints its own selected text (a DatePicker day) draws that text itself rather than asking Text for a colour it has no tone for. Locked, it still keeps its --ds-text-color hook on web and Lit, as every locked binding does, and the tone rule reads the hook. Each tone sets the hook’s default to the token’s own custom property — default is the bare var(--color-foreground) an inverse surface re-scopes, and onAction is var(--color-foreground-on-action) (camelCase to kebab-case) — so re-scoping --color-foreground on a container still recolours the default tone. TextForegroundContext exists on React Native only: it is exported from the rn Text.tsx for sibling components and not re-exported from the package index. The web Text.tsx has no such export.
colorcolor.foreground.onAction #ffffff #ffffffyesLocked, and locked means absent from the overridable type — so a composing parent that forwards { color: … } gets a compile error, not a silent no-op. A surface with its own foreground (Toast, Tooltip, Slider’s value bubble on color.inverse.foreground) therefore does not recolour Text at all: on web and Lit it re-scopes --color-foreground on its own container, which the tone resolves through and which touches no part of the child; on React Native, where there is no cascade, it provides the package-internal TextForegroundContext, which Text reads only while tone is default. onAction is not a stand-in for either — in dark mode it is near-white while the inverse foreground is near-black. A control that paints its own selected text (a DatePicker day) draws that text itself rather than asking Text for a colour it has no tone for. Locked, it still keeps its --ds-text-color hook on web and Lit, as every locked binding does, and the tone rule reads the hook. Each tone sets the hook’s default to the token’s own custom property — default is the bare var(--color-foreground) an inverse surface re-scopes, and onAction is var(--color-foreground-on-action) (camelCase to kebab-case) — so re-scoping --color-foreground on a container still recolours the default tone. TextForegroundContext exists on React Native only: it is exported from the rn Text.tsx for sibling components and not re-exported from the package index. The web Text.tsx has no such export.

Toast

Component doc

BindingTokenLightDarkLockedDescription
surfacecolor.inverse.surface #17181b #ecedeeyesInverted like Tooltip: dark on light, light on dark, so it floats above any page surface.
textcolor.inverse.foreground #f6f7f8 #17181byespart message: Text’s color is locked and has no inverse tone, so the toast re-scopes the foreground on its own toast container, not on the message part (--color-foreground on web and Lit, TextForegroundContext on React Native) and composes Text unchanged, as Tooltip does.
iconcolor.inverse.status.neutral #f6f7f8 #17181byespart icon: neutral renders no icon; the other tones render Icon name={tone} at its default size, with this token forwarded to Icon’s overrides.color, using the status step chosen to read on the inverse surface.
iconcolor.inverse.status.success #76d07a #00791dyespart icon: neutral renders no icon; the other tones render Icon name={tone} at its default size, with this token forwarded to Icon’s overrides.color, using the status step chosen to read on the inverse surface.
iconcolor.inverse.status.warning #e6ac3f #825b00yespart icon: neutral renders no icon; the other tones render Icon name={tone} at its default size, with this token forwarded to Icon’s overrides.color, using the status step chosen to read on the inverse surface.
iconcolor.inverse.status.danger #ff958d #b51d26yespart icon: neutral renders no icon; the other tones render Icon name={tone} at its default size, with this token forwarded to Icon’s overrides.color, using the status step chosen to read on the inverse surface.
actionColorcolor.inverse.link #99b5ff #243ab3yesThe action and dismiss Buttons are rendered with Button’s inverse prop (ghost variant), which is how a composite gets an on-inverse child without restyling it.
dismissColorcolor.inverse.link #99b5ff #243ab3yesThe dismiss and action Buttons are ghost + inverse, whose text is color.inverse.link; Toast never restyles them.
focusRingInversecolor.inverse.focus #99b5ff #243ab3yesFocus ring color on the inverse surface, replacing color.border.focus inside the toast.
radiusradius.md4px4pxno
shadowshadow.overlay0px 8px 24px 0px #0000002e0px 8px 24px 0px #00000080no
paddingBlockspace.sm8px8pxno
paddingInlinespace.md12px12pxno
gaplayout.gap.normal8px8pxnoBetween icon, message, action and dismiss.
stackGaplayout.gap.tight4px4pxnoBetween stacked toasts in the region. stackGap, regionInset and layer belong to the region (ToastRegion / ds-toast-region / ToastProvider) and are overridable through the region’s own overrides, not on a toast; their CSS hooks keep the toast prefix on web and Lit: --ds-toast-stack-gap, --ds-toast-region-inset, --ds-toast-layer. The region’s three keys form their own binding group, accepted by the region and absent from a toast’s overrides; where a platform has only one union (React Native), a standalone Toast accepts them and they do nothing.
regionInsetlayout.gutter24px24pxnopart region: Distance of the region from the viewport edge (bottom-start on wide screens, bottom center on phones; on web and Lit plus env(safe-area-inset-bottom)). “Wide” is the resolved px of layout.maxWidth.content read from the default theme at generation time and marked literal-ok, as Container does, since custom properties cannot be used in media queries.
maxWidthlayout.maxWidth.prose572px572pxno
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnofontFamily, fontSize and lineHeight are forward-only: they reach the message Text through its overrides, have no --ds-toast-* hook, and the value (override or this default) is always passed.
fontSizefont.size.md16px16pxno
lineHeightfont.lineHeight.normal1.51.5no
minTargetsize.target.min24px24pxyesThe minimum block size of the toast row, so a toast is never shorter than a touch target even with no buttons in it. The action and dismiss Buttons size their own targets; Toast never applies this to them.
layerlayer.toast400400noThe region’s stacking layer — one of the region’s three bindings, like stackGap and regionInset, and overridable only through the region.
entermotion.duration.base200ms200msnoRise and fade; instant under reduced motion.
enterOffsetspace.28px8pxnoDistance the toast rises during enter; none under reduced motion.
exitmotion.duration.fast120ms120msnoThe reverse of enter: sinks by enterOffset while fading; instant under reduced motion. Resolved at runtime like the two durations, so an override is honoured; an unresolvable value (jsdom, no theme CSS) removes the toast at once.

Toolbar

Component doc

BindingTokenLightDarkLockedDescription
backgroundcolor.background.subtle #f6f7f8 #2a2b2fyes
bordercolor.border #d9dbdd #414246no
borderWidthborder.width.thin1px1pxno
radiusradius.md4px4pxno
paddingInlinespace.28px8pxno
paddingBlockspace.14px4pxno
itemGaplayout.gap.normal8px8pxnoBetween adjacent controls, inside a group and between ungrouped top-level controls alike. An override replaces the value at both densities.
itemGap (density=compact)layout.gap.tight4px4pxnoBetween adjacent controls, inside a group and between ungrouped top-level controls alike. An override replaces the value at both densities.
groupGaplayout.gap.normal8px8pxnopart separator: Either side of a separator, replacing itemGap there (not added to it). Every platform applies it the same way: the row’s gap stays itemGap and the separator part gets inline padding (block padding when vertical) of groupGap − itemGap, clamped at 0, so an override smaller than itemGap has no effect. The clamp needs a zero length of its own: max(0px, calc(…)) on web and Lit (a literal-ok zero, not a token), Math.max(0, …) on React Native.
separatorLengthspace.520px20pxnopart separator: The Divider between groups is shorter than the toolbar height. Divider has no length binding, so this is the block size (inline size when vertical) of the separator wrapper element, which carries the part hook; the Divider inside it has orientation across the toolbar axis, spacing: none, and stretches to fill the wrapper. Being shorter than the toolbar, the separator is centred on the cross axis.
fadeWidthspace.624px24pxnoEdge fade for a scrolling row — overflow: scroll, and menu on React Native, which renders as scroll there; wrap never fades. The fade sits over the scrolling row only, inside the toolbar’s padding, so the border and the padding stay unclouded. Drawn as a mask-image gradient on web and Lit (so the content fades into whatever is behind the toolbar, with no second painted layer) and as a react-native-svg gradient from the toolbar background to transparent on React Native. Each edge fades only while content is hidden past it, re-checked on scroll and on size changes. The faded edges are physical — left and right, top and bottom on a vertical toolbar, which scrolls vertically — so RTL needs no special case.
focusRingcolor.border.focus #496ded #99b5ffyes
focusRingWidthborder.width.focus2px2pxyes

Tooltip

Component doc

BindingTokenLightDarkLockedDescription
surfacecolor.inverse.surface #17181b #ecedeeyesInverted: the tooltip is dark on light mode and light on dark mode, so it reads as a label, not a panel.
textcolor.inverse.foreground #f6f7f8 #17181byespart text: Text’s color is locked and its tones have no inverse value, so the bubble re-scopes the foreground on its own container and composes Text unchanged — the mechanism Text’s own color binding names, not an override and not a restyle.
radiusradius.sm2px2pxno
paddingBlockspace.14px4pxno
paddingInlinespace.28px8pxno
offsetspace.14px4pxnoGap between trigger and tooltip.
maxWidthspace.2080px80pxnoLonger text wraps. An override replaces the base token, not the whole expression: the rule is (override ?? space.20) × 3 on every platform.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnoForward-only, as fontSize and lineHeight are: the value (this default or an override) is always passed to the bubble’s Text through its overrides, and there is no --ds-tooltip-font-family hook. A consumer restyles tooltip typography through Text’s own --ds-text-* hooks, which the whole subtree inherits.
fontSizefont.size.sm13px13pxnoForward-only like fontFamily.
lineHeightfont.lineHeight.normal1.51.5noForward-only like fontFamily.
shadowshadow.raised0px 1px 3px 0px #0000001a0px 1px 3px 0px #00000059no
layerlayer.toast400400noTooltips sit above everything, including dialogs, because they describe controls inside them. overlay.layer: tooltip names the stacking category; this token is its value.
entermotion.duration.fast120ms120msnoFade in; instant under reduced motion.
exitmotion.duration.fast120ms120msnoFade out, opacity only with no slide; instant under reduced motion.

Tree

Component doc

BindingTokenLightDarkLockedDescription
indentspace.520px20pxnopart indent: Per level on the node row.
rowHeightsize.target.min24px24pxyes
rowPaddingInlinespace.28px8pxno
rowRadiusradius.sm2px2pxno
rowGaplayout.gap.tight4px4pxnoBetween the expand button, icon, label and badge; the checkbox is followed by checkboxGap instead.
rowHovercolor.action.ghost.backgroundHover #f6f7f8 #2a2b2fnostate hover: On native it is the pressed fill (and the react-native-web hover), swapped instantly; a selected row keeps rowSelected.
rowSelectedcolor.background.strong #ecedee #414246yesFill on the selected node, and on aria-checked=“true” nodes in multiple mode. An indeterminate (mixed) parent gets neither this fill nor the start-edge bar — its dash glyph is the only signal.
rowSelectedBordercolor.control.selectedBackground #3553d2 #496dedyesStart-edge bar on the selected (or checked) node, as Table and DataGrid. Drawn over the row at the logical start and mirrored in RTL, by whichever means the platform has that does not move the content (an inset shadow on the row, or an absolutely positioned box).
rowSelectedBorderWidthborder.width.focus2px2pxyes
labelColorcolor.foreground #2a2b2f #ecedeeyespart label: The label Text’s default tone; Text.color is locked, so it is not forwarded.
labelSelectedWeightfont.weight.medium500500nopart label: Forwarded to the label Text as overrides.fontWeight when selected. When a node has href the label Text only wraps the composed link and the weight is not forwarded, so a selected navigation node is marked by the row rather than by heavier text.
headingSizefont.size.md16px16pxnopart heading: The visible label Heading; forwarded as overrides.fontSize, which is what the label actually renders at. The Heading is composed at size: md, the size this token matches, so the two agree at the default and an override of headingSize alone moves the rendered size without moving the Heading’s own size. The gap below it is the Heading’s own marginBlockEnd.
iconColorcolor.foreground.muted #56585c #bcbec1yespart icon.
badgeColorcolor.foreground.muted #56585c #bcbec1yespart badge: The badge Text’s tone="muted"; Text.color is locked, so it is not forwarded.
badgeSizefont.size.xs11px11pxnopart badge: Forwarded to the badge Text as overrides.fontSize.
expandButtonSizesize.target.min24px24pxyespart expandButton: The expandButton part is a wrapper the tree owns, this size on both axes, around an unmodified ghost Button centred inside it (Button has no size override and keeps its own data-part, and a sm Button is smaller than this, so the target is the wrapper, not the Button); the chevron takes the Button’s own color. TreeGrid’s expandButton is the same wrapper-around-a-Button shape.
guideLinecolor.border #d9dbdd #414246noOne vertical line per open parent, drawn down that parent’s group, at rowPaddingInline + indent × (parent level − 1) + expandButtonSize / 2 from the row’s start — the centre of that parent’s chevron. No elbows, no termination at the last child, as TreeGrid.
guideLineWidthborder.width.thin1px1pxno
checkboxGaplayout.gap.tight4px4pxnopart checkbox: In multiple mode the row reads chevron, checkbox, icon, label, badge; this is the gap after the checkbox, so on a node with an icon it falls between the checkbox and the icon. Every other gap in the row is rowGap, which means the row is two nested groups rather than one gap (checkbox beside the rest at checkboxGap; chevron, icon, label and badge at rowGap) — one flex gap cannot differ per pair. In single and none no checkbox renders and this binding has no effect.
checkboxSizespace.416px16pxnopart checkbox: The drawn checkbox glyph in multiple mode (the treeitem is the control; no Checkbox component).
checkboxBordercolor.control.border #939599 #707176yespart checkbox: Unchecked edge; checked or mixed, the border takes checkboxSelected so the fill has no contrasting edge.
checkboxBorderWidthborder.width.thin1px1pxnopart checkbox.
checkboxBackgroundcolor.control.background #ffffff #2a2b2fnopart checkbox.
checkboxSelectedcolor.control.selectedBackground #3553d2 #496dedyespart checkbox: Fill when checked or indeterminate.
checkboxMarkcolor.control.selectedForeground #ffffff #000000yespart checkbox: The check or dash Icon on the fill.
checkboxRadiusradius.sm2px2pxnopart checkbox.
fontFamilyfont.family.bodysystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifsystem-ui, -apple-system, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serifnoSets the container’s font, and the resolved value (the override when there is one, the token otherwise) is always forwarded to the label Text as overrides.fontFamily, so the root hook and the composed label never disagree. The lazy placeholder and the empty-state Text take the same three resolved values at tone="muted"; a platform with no inherited font (React Native) forwards them there too rather than leaving Text’s own defaults.
fontSizefont.size.sm13px13pxnoThe container’s size and, resolved the same way, the label Text’s overrides.fontSize.
lineHeightfont.lineHeight.normal1.51.5noThe container’s line height and, resolved the same way, the label Text’s overrides.lineHeight.
minTargetsize.target.min24px24pxyes
focusRingcolor.border.focus #496ded #99b5ffyesDrawn around the whole node — the chevron included, since the row is one visual target — not around nodeRow alone. It follows the treeitem’s own focus only: where the chevron Button is a focus target of its own (react-native-web), focusing it shows the Button’s own treatment and leaves the node’s ring unlit.
focusRingWidthborder.width.focus2px2pxyes
disabledOpacityopacity.disabled0.50.5no
transitionmotion.duration.fast120ms120msnoChevron rotation, and the row hover where the platform has one to animate (web and Lit); groups appear instantly, and the native pressed fill is swapped instantly as rowHover says. The collapsed chevron is mirrored in RTL.

TreeGrid

Component doc

BindingTokenLightDarkLockedDescription
indentspace.520px20pxnopart indent: Per level: the indent part is a spacer at the start of the row header, indent × (level − 1) wide, so the cell keeps its own inline padding. Level 1 has none.
expandButtonSizesize.target.min24px24pxyespart expandButton: The expandButton part is the wrapper TreeGrid owns, this size on both axes, around an unmodified ghost Button centred inside it — the wrapper is the target and the inline width the guide lines align to, exactly as Tree’s expandButton, because Button has no size-override binding and a child is never restyled. minTarget is the locked row-height floor.
expandGaplayout.gap.tight4px4pxnoBetween the expand button and the row header text.
guideLinecolor.border #d9dbdd #414246noOne vertical line per ancestor level, drawn the full height of every descendant row and aligned with that ancestor’s expand button (indent guides; no elbows, no termination at the last child). Always drawn, at both densities. Drawn on the row rather than inside the row header cell, whose overflow is hidden and which may be pinned — either would clip them — at cellPaddingInline + indent × (ancestor level − 1) + expandButtonSize / 2 from the row header’s start, offset by the selection column when there is one.
cellPaddingInlinespace.28px8pxnoAs DataGrid; also the base of the guide line offset.
fixedHeightspace.2080px80pxnoAs DataGrid: the height for height: fixed, sizing the whole component with the scroll region taking what the caption and status bar leave; overridable by design.
guideLineWidthborder.width.thin1px1pxno
parentWeightfont.weight.medium500500noRow headers of rows with children.
loadingColorcolor.foreground.muted #56585c #bcbec1yesThe lazy-loading placeholder text in a just-expanded row. It reaches the composed Text as tone="muted", which resolves to this token — Text.color is locked, so it is never forwarded as a colour.
transitionmotion.duration.fast120ms120msnoThe chevron rotation on expand, and nothing else TreeGrid owns: rows appear instantly (no height animation in a virtualized grid), and the row hover and editor-open transitions are DataGrid’s, at DataGrid’s own token.
focusRingcolor.border.focus #496ded #99b5ffyesDrawn by the scrollRegion, as DataGrid’s focusRing is, while the grid inside it has focus — so the region’s own overflow never clips the ring. The ring on the active cell is DataGrid’s cellFocusRing, which TreeGrid inherits with the rest; these two bindings have no part of their own because the part they draw on is the one DataGrid names.
focusRingWidthborder.width.focus2px2pxyes
minTargetsize.target.min24px24pxyes