@teamblind-chorus/ui 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/agents/AGENTS.md +6 -6
- package/agents/DESIGN.md +245 -244
- package/agents/LOVABLE.md +40 -11
- package/agents/catalog.md +10 -8
- package/agents/components/avatar-rail/avatar-rail.md +2 -4
- package/agents/components/avatar-rail/avatar-rail.spec.json +27 -12
- package/agents/components/badge/role.md +7 -9
- package/agents/components/badge/role.spec.json +6 -6
- package/agents/components/badge/update.md +6 -8
- package/agents/components/badge/update.spec.json +5 -5
- package/agents/components/banner/banner.family.json +3 -1
- package/agents/components/banner/banner.md +66 -15
- package/agents/components/banner/banner.spec.json +37 -14
- package/agents/components/bottom-sheet/bottom-sheet.md +4 -6
- package/agents/components/bottom-sheet/bottom-sheet.spec.json +5 -5
- package/agents/components/bubble/bubble.md +8 -10
- package/agents/components/bubble/bubble.spec.json +11 -11
- package/agents/components/button/button.md +1 -1
- package/agents/components/button/check.md +9 -11
- package/agents/components/button/check.spec.json +25 -8
- package/agents/components/button/fab.md +7 -9
- package/agents/components/button/fab.spec.json +27 -10
- package/agents/components/button/group.spec.json +4 -4
- package/agents/components/button/icon.md +21 -23
- package/agents/components/button/icon.spec.json +29 -12
- package/agents/components/button/standard.md +40 -42
- package/agents/components/button/standard.spec.json +37 -20
- package/agents/components/button/text.md +21 -23
- package/agents/components/button/text.spec.json +30 -13
- package/agents/components/button/toggle.md +7 -9
- package/agents/components/button/toggle.spec.json +27 -10
- package/agents/components/button/toolbar.md +24 -26
- package/agents/components/button/toolbar.spec.json +10 -12
- package/agents/components/carousel/carousel.md +1 -1
- package/agents/components/carousel/post.md +15 -21
- package/agents/components/carousel/post.spec.json +17 -17
- package/agents/components/carousel/profile.md +9 -45
- package/agents/components/carousel/profile.spec.json +17 -17
- package/agents/components/chip/chip.md +1 -1
- package/agents/components/chip/filter.md +22 -24
- package/agents/components/chip/filter.spec.json +34 -11
- package/agents/components/chip/tag.md +22 -24
- package/agents/components/chip/tag.spec.json +36 -13
- package/agents/components/dialog/dialog.md +1 -3
- package/agents/components/dialog/dialog.spec.json +3 -3
- package/agents/components/directory-list/directory-list.md +1 -3
- package/agents/components/directory-list/directory-list.spec.json +2 -2
- package/agents/components/divider/divider.family.json +1 -1
- package/agents/components/divider/divider.md +12 -14
- package/agents/components/divider/divider.spec.json +8 -8
- package/agents/components/empty-state/empty-state.family.json +28 -0
- package/agents/components/empty-state/empty-state.md +69 -0
- package/agents/components/empty-state/empty-state.spec.json +87 -0
- package/agents/components/feed/ad.md +2 -4
- package/agents/components/feed/ad.spec.json +10 -10
- package/agents/components/feed/post.md +41 -43
- package/agents/components/feed/post.spec.json +35 -39
- package/agents/components/form-field/form-field.md +1 -1
- package/agents/components/form-field/input.md +32 -34
- package/agents/components/form-field/input.spec.json +39 -31
- package/agents/components/form-field/search.md +2 -4
- package/agents/components/form-field/search.spec.json +24 -16
- package/agents/components/form-field/select.md +18 -20
- package/agents/components/form-field/select.spec.json +36 -27
- package/agents/components/form-field/textarea.md +3 -5
- package/agents/components/form-field/textarea.spec.json +37 -29
- package/agents/components/header/main.md +4 -6
- package/agents/components/header/main.spec.json +3 -3
- package/agents/components/header/sub.md +6 -8
- package/agents/components/header/sub.spec.json +3 -3
- package/agents/components/list/accordion.md +34 -45
- package/agents/components/list/accordion.spec.json +26 -17
- package/agents/components/list/entry.md +59 -81
- package/agents/components/list/entry.spec.json +37 -21
- package/agents/components/list/list.md +2 -2
- package/agents/components/list/radio.md +13 -20
- package/agents/components/list/radio.spec.json +33 -18
- package/agents/components/list/standard.md +88 -64
- package/agents/components/list/standard.spec.json +52 -20
- package/agents/components/metadata/compact.md +4 -6
- package/agents/components/metadata/compact.spec.json +6 -6
- package/agents/components/metadata/metadata.md +1 -1
- package/agents/components/metadata/standard.md +12 -14
- package/agents/components/metadata/standard.spec.json +10 -10
- package/agents/components/nav-card/nav-card.md +25 -27
- package/agents/components/nav-card/nav-card.spec.json +25 -16
- package/agents/components/nav-list/nav-list.md +2 -8
- package/agents/components/nav-list/nav-list.spec.json +3 -3
- package/agents/components/navigation-bar/main.md +9 -11
- package/agents/components/navigation-bar/main.spec.json +6 -6
- package/agents/components/navigation-bar/search.md +6 -8
- package/agents/components/navigation-bar/search.spec.json +9 -9
- package/agents/components/navigation-bar/sub.md +9 -11
- package/agents/components/navigation-bar/sub.spec.json +7 -7
- package/agents/components/page-shell/page-shell.family.json +1 -1
- package/agents/components/page-shell/page-shell.md +33 -0
- package/agents/components/page-shell/page-shell.spec.json +85 -0
- package/agents/components/pagination/pagination.family.json +1 -1
- package/agents/components/pagination/pagination.md +3 -3
- package/agents/components/pagination/pagination.spec.json +5 -5
- package/agents/components/profile-header/profile-header.md +9 -11
- package/agents/components/profile-header/profile-header.spec.json +9 -9
- package/agents/components/progress/progress.family.json +1 -1
- package/agents/components/progress/progress.md +5 -5
- package/agents/components/progress/progress.spec.json +8 -8
- package/agents/components/side-sheet/side-sheet.md +11 -13
- package/agents/components/side-sheet/side-sheet.spec.json +3 -3
- package/agents/components/skeleton/skeleton.md +7 -9
- package/agents/components/skeleton/skeleton.spec.json +5 -5
- package/agents/components/spinner/spinner.family.json +27 -0
- package/agents/components/spinner/spinner.md +96 -0
- package/agents/components/spinner/spinner.spec.json +82 -0
- package/agents/components/status-tag/status-tag.md +7 -9
- package/agents/components/status-tag/status-tag.spec.json +5 -5
- package/agents/components/suggestion-list/suggestion-list.md +3 -7
- package/agents/components/suggestion-list/suggestion-list.spec.json +8 -12
- package/agents/components/switch/switch.md +12 -14
- package/agents/components/switch/switch.spec.json +23 -15
- package/agents/components/tab-bar/tab-bar.md +9 -11
- package/agents/components/tab-bar/tab-bar.spec.json +37 -23
- package/agents/components/tabs/rounded.md +6 -8
- package/agents/components/tabs/rounded.spec.json +34 -13
- package/agents/components/tabs/segmented.md +4 -6
- package/agents/components/tabs/segmented.spec.json +4 -8
- package/agents/components/tabs/underline.md +9 -11
- package/agents/components/tabs/underline.spec.json +31 -14
- package/agents/components/thumbnail/thumbnail.md +5 -7
- package/agents/components/thumbnail/thumbnail.spec.json +8 -8
- package/agents/components/toast/toast.md +5 -7
- package/agents/components/toast/toast.spec.json +3 -3
- package/agents/components/tooltip/tooltip.md +6 -8
- package/agents/components/tooltip/tooltip.spec.json +4 -4
- package/agents/manifest.json +8 -6
- package/agents/tokens.usage.json +71 -226
- package/agents/usage.json +12 -0
- package/dist/index.cjs +531 -262
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -13
- package/dist/index.d.ts +57 -13
- package/dist/index.js +530 -263
- package/dist/index.js.map +1 -1
- package/dist/styles.css +560 -379
- package/eslint/rules.js +7 -7
- package/package.json +2 -3
- package/agents/anti-patterns.md +0 -533
- package/agents/compose.md +0 -240
- package/agents/images.md +0 -66
package/agents/DESIGN.md
CHANGED
|
@@ -42,7 +42,7 @@ Chorus is the design language of a community product with high text volume and m
|
|
|
42
42
|
Chorus follows a **three-tier** model — *reference → system → component* — with deliberate discipline about when each tier earns a token.
|
|
43
43
|
|
|
44
44
|
- **Reference tier** ([`reference.json`](schema/tokens/reference.json), namespaced under `ref.*`) — raw palettes and scales with no opinion about usage. `ref.palette.neutral.500`, `ref.fontSize.200`, `ref.space.400`. The material.
|
|
45
|
-
- **System tier** ([`system.json`](schema/tokens/system.json), namespaced under `sys.*`) — semantic roles consuming the reference tier via `{ref.palette.*}` / `{ref.space.*}` references. `sys.color.
|
|
45
|
+
- **System tier** ([`system.json`](schema/tokens/system.json), namespaced under `sys.*`) — semantic roles consuming the reference tier via `{ref.palette.*}` / `{ref.space.*}` references. `sys.color.text.default`, `sys.color.surface.default`, `sys.layout.page.md`, `sys.elevation.floating`. The vocabulary product surfaces speak in.
|
|
46
46
|
- **Component tier** ([`component.json`](schema/tokens/component.json), namespaced under `comp.*`) — per-component tokens that bind system roles to a component's contract. Currently illustrative-only (a hypothetical `comp.button.primary.container` / `comp.button.primary.label` pair would belong here) and ships empty by design — see [Current state of `comp.*`](#current-state-of-comp). Reserved for components reused widely enough that naming the composition earns its keep.
|
|
47
47
|
|
|
48
48
|
CSS variables emit with the full prefix preserved (`var(--sys-color-primary)`, `var(--ref-space-200)`) so tier identity is explicit at the call site — `var(--ref-…)` in a component is a code-review signal that the component reached past the system tier.
|
|
@@ -67,11 +67,19 @@ CSS custom properties follow `--<tier>-<group>-<name>`: `var(--sys-color-primary
|
|
|
67
67
|
|
|
68
68
|
### Color
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
Seven solid hues plus two opacity overlays, organized into role clusters that always pair a background with its foreground — components consume system roles, never the raw palette.
|
|
71
|
+
|
|
72
|
+
**Role-based token names.** `sys.color` uses role names that match the Figma Semantic collection: `text.*`, `icon.*`, `border.*`, `surface.*`, `background.*`, `overlay.*`. The M3→role migration is **complete** — the old Material-3 names (`onSurface`, `primary`, `surfaceContainerHigh`, `outline`…) are fully retired across web, native, and Figma; the historical mapping lives in [`ROLE-MIGRATION.md`](tokens/ROLE-MIGRATION.md). Every role carries its own light and dark value; components consume roles, never the raw palette.
|
|
73
|
+
|
|
74
|
+
**Locked status & accent policies:**
|
|
75
|
+
|
|
76
|
+
- **Status = 3 hues.** `success` is the **blue** family (not green) — info and success share blue, so a success state **must pair a checkmark (✓) icon** to carry the positive meaning colour alone can't. `warning` = yellow, `danger` = red. Green / teal / purple are categorical / decorative accents only.
|
|
77
|
+
- **Accent text = label only.** Accent hues at the 500 step clear only ~3:1 on white, so accent colours may paint **labels** (large ≥18pt, or bold ≥14pt) — **never body / paragraph text**. `subtle` / `subtlest` accents are background / decoration, never text.
|
|
78
|
+
- **Focus = neutral, single ring.** One `border.focused` ring, 1px, neutral-toned (near-black in light, mid-grey in dark) — no coloured stroke and no white inset counter-ring.
|
|
71
79
|
|
|
72
80
|
#### Reference palettes
|
|
73
81
|
|
|
74
|
-
|
|
82
|
+
Seven solid palettes share a 0–1000 lightness curve, tuned so the same numeric step lands at a perceptually similar brightness across hues. Pairing a 50–400 background with a 700–900 foreground (or vice versa) clears WCAG AA 4.5:1 for body text across every palette.
|
|
75
83
|
|
|
76
84
|
##### Lightness ramp
|
|
77
85
|
|
|
@@ -87,12 +95,13 @@ Step bands across all hues:
|
|
|
87
95
|
|
|
88
96
|
| Palette | 500 (canonical) | Role |
|
|
89
97
|
|----------|-----------------|---------------------------------------------------|
|
|
90
|
-
| `neutral` | `#
|
|
91
|
-
| `blue` | `#
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
98
|
+
| `neutral` | `#7b8496` | Text, surfaces, borders, dark UI chrome |
|
|
99
|
+
| `blue` | `#007aff` | Primary · info · **success** · links |
|
|
100
|
+
| `red` | `#e83a3a` | Brand · error · destructive |
|
|
101
|
+
| `yellow` | `#ffbc00` | Warning / caution |
|
|
102
|
+
| `green` | `#1fa86b` | Positive accent (categorical) |
|
|
103
|
+
| `teal` | `#04ceae` | Categorical accent / charts |
|
|
104
|
+
| `purple` | `#452cc7` | Categorical / decorative |
|
|
96
105
|
|
|
97
106
|
**Only system tokens may reference these palette steps.** Components never consume `palette.*` directly. Document a new role here in DESIGN.md before adding the JSON entry.
|
|
98
107
|
|
|
@@ -109,163 +118,148 @@ The ramp partitions into three functional bands:
|
|
|
109
118
|
| Band | Steps | Alpha values | Used by |
|
|
110
119
|
|-----------|--------------|-------------------------|--------------------------------------------------------------------------|
|
|
111
120
|
| Endpoint | `0` | 0% (transparent) | Reset / fully transparent overlays |
|
|
112
|
-
| Veil | `50–600` | 4 / 6 / 8 / 12 / 16 / 20 / 24% | `elevation.*` shadow alphas, `state.*` overlay opacities
|
|
113
|
-
| Scrim | `700–900` | 40 / 64 / 80% | `
|
|
114
|
-
| Endpoint | `1000` | 100% (fully opaque) |
|
|
121
|
+
| Veil | `50–600` | 4 / 6 / 8 / 12 / 16 / 20 / 24% | `elevation.*` shadow alphas, `state.*` overlay opacities, `overlay.hover` / `overlay.pressed` |
|
|
122
|
+
| Scrim | `700–900` | 40 / 64 / 80% | `overlay.scrim`, heavy modal / drawer dim |
|
|
123
|
+
| Endpoint | `1000` | 100% (fully opaque) | Fully-opaque overlays and shadow ink |
|
|
115
124
|
|
|
116
|
-
**Contrast guidance**: veil-band overlays are low-emphasis — foreground text (`
|
|
125
|
+
**Contrast guidance**: veil-band overlays are low-emphasis — foreground text (`text.default`, near-black) stays readable. Scrim-band and `1000` are strong overlays — use inverse text (`text.inverse`, near-white).
|
|
117
126
|
|
|
118
127
|
`palette.white.*` mirrors `palette.black.*` for dark-mode use: composite over dark backgrounds so the surface tint shows through.
|
|
119
128
|
|
|
120
|
-
####
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
The
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
#### Semantic roles
|
|
130
|
+
|
|
131
|
+
The role layer is the only colour surface components touch. Roles are grouped into six families — **Text**, **Icon**, **Border**, **Surface**, **Background**, **Overlay** — and within a family into three intents:
|
|
132
|
+
|
|
133
|
+
- **Neutral** — the reading / structure hierarchy, no status meaning. Text and icon fade from strong to weak (`default` → `subtle` → `subtlest`); border boldens with emphasis (`default` → `bold` → `boldest`). The two directions are intentional: type recedes as it de-emphasizes, an enclosure strengthens as it emphasizes.
|
|
134
|
+
- **Function** — encodes meaning: interaction (`link`, `mention`, `primary`, `focused`, `selected`) and the three status hues (`danger` red, `success` blue, `warning` yellow).
|
|
135
|
+
- **Accent** — decorative hue for labels, glyphs, and category coding only (see the accent-text-label-only policy above).
|
|
136
|
+
|
|
137
|
+
Every value below lists its light / dark primitive. The pairing rule is the same everywhere: a foreground role (`text.*` / `icon.*`) sits on a surface or background role, and the two must clear WCAG AA against the actual rendered fill in **both** modes.
|
|
138
|
+
|
|
139
|
+
##### Text
|
|
140
|
+
|
|
141
|
+
| Token | Role | → light / dark |
|
|
142
|
+
|-------|------|----------------|
|
|
143
|
+
| `text.default` | Primary body text — body copy, headings, default icons. | `neutral.1000` / `neutral.100` |
|
|
144
|
+
| `text.subtle` | Secondary text — navigation, supporting labels, captions. | `neutral.600` / `neutral.400` |
|
|
145
|
+
| `text.subtlest` | Quietest text — metadata, placeholders, supplementary notes. | `neutral.400` / `neutral.500` |
|
|
146
|
+
| `text.disabled` | Text in a disabled state. | `neutral.300` / `neutral.600` |
|
|
147
|
+
| `text.inverse` | Light text on dark / coloured backgrounds. | `neutral.0` / `neutral.1000` |
|
|
148
|
+
| `text.onFill` | Text on solid fills (buttons, badges). | `neutral.0` / `neutral.0` |
|
|
149
|
+
| `text.link` | Link text. | `blue.500` / `blue.400` |
|
|
150
|
+
| `text.mention` | Mention (@user) text. | `blue.400` / `blue.400` |
|
|
151
|
+
| `text.brand` | Brand-emphasis text — labels / badges only, never body. | `brand.red` / `brand.red` |
|
|
152
|
+
| `text.danger` | Danger / error text — error messages, input-validation notices. | `red.500` / `red.400` |
|
|
153
|
+
| `text.success` | Success text — save-complete, validation-passed notices. | `blue.500` / `blue.400` |
|
|
154
|
+
| `text.warning` | Warning text — non-blocking caution labels. | `yellow.700` / `yellow.300` |
|
|
155
|
+
| `text.accent.blue` | Blue accent text — labels only. | `blue.500` / `blue.400` |
|
|
156
|
+
| `text.accent.red` | Red accent text — labels only. | `red.400` / `red.400` |
|
|
157
|
+
| `text.accent.teal` | Teal accent text — labels only. | `teal.500` / `teal.400` |
|
|
158
|
+
|
|
159
|
+
##### Icon
|
|
160
|
+
|
|
161
|
+
| Token | Role | → light / dark |
|
|
162
|
+
|-------|------|----------------|
|
|
163
|
+
| `icon.default` | Default icon — the darkest neutral glyph. | `neutral.1000` / `neutral.200` |
|
|
164
|
+
| `icon.subtle` | Secondary icon — one step quieter. | `neutral.600` / `neutral.400` |
|
|
165
|
+
| `icon.subtlest` | Quietest icon — decorative / supporting glyphs. | `neutral.400` / `neutral.500` |
|
|
166
|
+
| `icon.disabled` | Icon in a disabled state. | `neutral.200` / `neutral.600` |
|
|
167
|
+
| `icon.inverse` | Light icon on dark / coloured backgrounds. | `neutral.0` / `neutral.1000` |
|
|
168
|
+
| `icon.onFill` | Icon on solid fills. | `neutral.0` / `neutral.0` |
|
|
169
|
+
| `icon.brand` | Brand icon — logomark / brand glyphs. | `brand.red` / `brand.red` |
|
|
170
|
+
| `icon.danger` | Danger / error icon. | `red.500` / `red.400` |
|
|
171
|
+
| `icon.success` | Success icon. | `blue.500` / `blue.400` |
|
|
172
|
+
| `icon.warning` | Warning icon. | `yellow.500` / `yellow.400` |
|
|
173
|
+
|
|
174
|
+
Accent icons carry six hues, each in a `default` and a `subtle` step, for standalone decorative glyphs (status marks, category tags, favourite stars). Use an accent icon only when the glyph carries its *own* hue; an icon acting as a control's foreground inherits the surrounding `icon.*` / `text.onFill` role instead.
|
|
175
|
+
|
|
176
|
+
| Hue | `default` (L / D) | `subtle` (L / D) |
|
|
177
|
+
|-----|-------------------|------------------|
|
|
178
|
+
| `icon.accent.blue` | `blue.500` / `blue.400` | `blue.300` / `blue.600` |
|
|
179
|
+
| `icon.accent.red` | `red.400` / `red.400` | `red.300` / `red.600` |
|
|
180
|
+
| `icon.accent.yellow` | `yellow.500` / `yellow.400` | `yellow.200` / `yellow.600` |
|
|
181
|
+
| `icon.accent.teal` | `teal.500` / `teal.400` | `teal.200` / `teal.600` |
|
|
182
|
+
| `icon.accent.green` | `green.500` / `green.400` | `green.200` / `green.600` |
|
|
183
|
+
| `icon.accent.purple` | `purple.500` / `purple.400` | `purple.200` / `purple.600` |
|
|
184
|
+
|
|
185
|
+
##### Border
|
|
186
|
+
|
|
187
|
+
| Token | Role | → light / dark |
|
|
188
|
+
|-------|------|----------------|
|
|
189
|
+
| `border.default` | Light default border — feed / list separators, card edges. The most common default. | `neutral.100` / `neutral.900` |
|
|
190
|
+
| `border.bold` | One step bolder — encloses cards, sections, comments, inputs. | `neutral.200` / `neutral.800` |
|
|
191
|
+
| `border.boldest` | High-emphasis border — emphasized separators. | `neutral.500` / `neutral.600` |
|
|
192
|
+
| `border.primary` | Primary border — emphasis stroke for primary actions. | `blue.500` / `blue.400` |
|
|
193
|
+
| `border.focused` | Focus ring — keyboard-focus indicator, neutral single ring. | `neutral.1000` / `neutral.500` |
|
|
194
|
+
| `border.selected` | Selected border — outline for selected items. | `neutral.1000` / `neutral.100` |
|
|
195
|
+
| `border.danger` | Danger border — error input-field outline. | `red.400` / `red.400` |
|
|
196
|
+
| `border.success` | Success border — validated-input outline. | `blue.400` / `blue.400` |
|
|
197
|
+
| `border.warning` | Warning border. | `yellow.400` / `yellow.400` |
|
|
198
|
+
|
|
199
|
+
`border.focused` and `border.selected` are **neutral**, not blue — a near-black stroke in light and a mid-grey in dark. Selection and focus read by shape and weight rather than by competing with `border.primary`'s hue.
|
|
200
|
+
|
|
201
|
+
##### Surface
|
|
202
|
+
|
|
203
|
+
The two base planes everything sits on. Elevation is expressed by `elevation.*` shadows, **not** by an ever-brightening surface ladder — the Chorus brand goal is *calm and trustworthy*, and stacking tonal lifts feels showy. A raised element (modal, sheet) separates via `scrim` + shadow, and its surface tone stays at `surface.default`.
|
|
204
|
+
|
|
205
|
+
| Token | Role | → light / dark |
|
|
206
|
+
|-------|------|----------------|
|
|
207
|
+
| `surface.default` | Base plane for pages and cards — the "ground" of the UI. | `neutral.0` / `neutral.1000` |
|
|
208
|
+
| `surface.sunken` | One step below the page base — gutters, wells, inset regions. | `neutral.100` / `neutral.900` |
|
|
209
|
+
|
|
210
|
+
##### Background
|
|
211
|
+
|
|
212
|
+
Container and banner fills that tint a region without being the page ground.
|
|
213
|
+
|
|
214
|
+
| Token | Role | → light / dark |
|
|
215
|
+
|-------|------|----------------|
|
|
216
|
+
| `background.neutral` | Neutral container fill — chips, tags, general content cards. No status meaning. | `neutral.100` / `neutral.800` |
|
|
217
|
+
| `background.inverse` | Inverse background — dark tooltips, inverted regions. | `neutral.1000` / `neutral.0` |
|
|
218
|
+
| `background.disabled` | Disabled fill. | `neutral.50` / `neutral.900` |
|
|
219
|
+
| `background.primary` | Primary fill — primary buttons and actions. | `blue.500` / `blue.500` |
|
|
220
|
+
| `background.selected` | Selected background — selected list rows, tabs. | `blue.50` / `blue.900` |
|
|
221
|
+
| `background.information` | Information banner — informational callouts / notices. | `blue.100` / `blue.900` |
|
|
222
|
+
| `background.danger` | Danger banner — soft red error surface. | `red.100` / `red.900` |
|
|
223
|
+
| `background.success` | Success banner — soft surface signalling success (blue, not green). | `blue.50` / `blue.900` |
|
|
224
|
+
| `background.warning` | Warning banner — soft yellow surface. | `yellow.50` / `yellow.900` |
|
|
225
|
+
|
|
226
|
+
##### Overlay
|
|
227
|
+
|
|
228
|
+
Translucent layers composited over content — the host surface bleeds through. **State** overlays convey interaction feedback; **Scrim** overlays darken or protect.
|
|
229
|
+
|
|
230
|
+
| Token | Role | → light / dark |
|
|
231
|
+
|-------|------|----------------|
|
|
232
|
+
| `overlay.pressed` | Pressed-state layer. | `black.300` (12%) / `black.300` (12%) |
|
|
233
|
+
| `overlay.hover` | Hover-state layer. | `white.300` (12%) / `white.300` (12%) |
|
|
234
|
+
| `overlay.scrim` | Modal / sheet backdrop dim — darkens the screen behind. | `overlay.black-32` / `overlay.black-75` |
|
|
235
|
+
| `overlay.image` | Image protection overlay — guards text legibility over photos. | `overlay.black-3` / `overlay.black-3` |
|
|
236
|
+
| `overlay.illustration` | Illustration dim — sinks illustrations back in dark mode. | `black.0` (transparent) / `black.400` (16%) |
|
|
237
|
+
|
|
238
|
+
##### Composition & pairing
|
|
239
|
+
|
|
240
|
+
Colour is composed as **foreground role on surface / background role** — never a hand-tuned value at the call site.
|
|
241
|
+
|
|
242
|
+
- **Neutral text on any surface.** `text.default` / `subtle` / `subtlest` clear AA against `surface.default`, `surface.sunken`, and `background.neutral` in both modes. Pick the emphasis tier, not the hex.
|
|
243
|
+
- **Status recipes pair a Function background with its Function foreground.** A danger surface is `background.danger` + `text.danger` (and `border.danger` for the outline); success is `background.success` + `text.success`; information is `background.information` + `text.link`; warning is `background.warning` + `text.warning`. Because `success` resolves to blue, a success state **must** also carry a ✓ glyph.
|
|
244
|
+
- **Solid fills use the on-fill pair.** `background.primary` fill takes `text.onFill` / `icon.onFill` (which stay light in both modes).
|
|
245
|
+
- **Never `color-mix(<accent> N%, <surface>)` to fake a tint.** The `background.*` role already resolves to the tuned soft tone that clears AA against its paired foreground; an alpha mix bypasses that contract. If a canonical pair gives a poor visual, retune the token value in [`system.json`](tokens/system.json) — never break the pair at the call site.
|
|
246
|
+
|
|
247
|
+
**Contrast refusal contract (for agents and humans).** The role pairs exist so contrast is *never* a per-call-site decision. When a composition strays outside the shipped pairs (a custom-tinted hero, a brand glyph on a non-`surface` host), the call site MUST clear WCAG **AA — 4.5:1 for normal text, 3:1 for ≥18pt or Semibold ≥14pt text, and 3:1 for non-text glyphs and graphic boundaries** against the actual rendered fill in BOTH modes. If the proposed foreground fails, **change the host fill to a role that already pairs** rather than hand-tuning the foreground. Zero-tolerance failure modes (black-on-black, white-on-yellow, translucent accent glyph on a colour-tinted host, `text.onFill` on a neutral `surface` fill) are enumerated in [`AGENTS.md` § Hard rules](../AGENTS.md#hard-rules) and trigger a regenerate, not a tweak.
|
|
136
248
|
|
|
137
249
|
**Allowed `color-mix` exceptions** — two and only two:
|
|
138
250
|
|
|
139
|
-
1. **State-overlay formula** — `
|
|
140
|
-
2. **Decorative gradient atmospherics** — an
|
|
141
|
-
|
|
142
|
-
###### Primary
|
|
143
|
-
|
|
144
|
-
| Token | Role |
|
|
145
|
-
|----------------------------|-------------------------------------------------------------------------------|
|
|
146
|
-
| `color.primary` | The brand color and highest-attention accent. Use sparingly for one dominant action per view (primary CTA, selected tab underline, active toggle fill, progress indicator). Two primary buttons in a view collapse the hierarchy. Resolves to `ref.palette.blue.500` in both modes — the brand hue is saturated enough to clear AA against `surface` in both light (white) and dark (`neutral.900`), so the CTA reads as the same blue across themes without a tonal nudge. |
|
|
147
|
-
| `color.onPrimary` | Foreground placed on top of `primary`. Label text, icons, and spinners inside primary-filled surfaces. Always pair with `primary`; never against a neutral surface. Resolves to `ref.palette.neutral.50`. |
|
|
148
|
-
| `color.primaryContainer` | Low-chroma tinted surface in the primary family. Selected-state list backgrounds, informational callouts, highlighted message bubbles, brand-flavored section banners. Safe on larger areas where `primary` would overwhelm. Resolves to `ref.palette.blue.50` (light) / `ref.palette.blue.900` (dark). The light value sits one step brighter than the other accent containers (`error` uses `*.100`) because primary is the most-used quartet in the product — a `blue.100` callout next to multiple active list rows on the same page felt heavier than the role asks for. The lighter step keeps the brand identity visible against `surface` while reading as a quiet, decorative tint rather than a filled banner. |
|
|
149
|
-
| `color.onPrimaryContainer` | Foreground for content placed on `primaryContainer`. Text, icons, and links inside primary-tinted surfaces. Resolves to `ref.palette.blue.600` (light) / `ref.palette.blue.400` (dark) — both stay in the saturated primary family so the foreground reads as *blue* on both tinted backgrounds, instead of collapsing to near-black on the light tint or muddying into the deep container on the dark tint. The dark step lifts one band higher than light's mirror would suggest because identical luminance gaps read darker on dark surfaces. The pair clears AA at ~9:1 against the lifted light container. |
|
|
150
|
-
|
|
151
|
-
###### Secondary
|
|
152
|
-
|
|
153
|
-
| Token | Role |
|
|
154
|
-
|------------------------------|-------------------------------------------------------------------------------|
|
|
155
|
-
| `color.secondary` | A neutral accent for supporting actions that should feel present but not brand-loud. Secondary CTAs paired beside a primary button, quiet filled controls, selection highlights where a colored brand fill would be distracting. Unlike the chromatic accents, this family inverts between light and dark modes. Resolves to `ref.palette.neutral.700` (light) / `ref.palette.neutral.300` (dark). |
|
|
156
|
-
| `color.onSecondary` | Foreground placed on top of `secondary`. Label text and icons inside secondary-filled surfaces. Resolves to `ref.palette.neutral.50` (light) / `ref.palette.neutral.900` (dark). |
|
|
157
|
-
| `color.secondaryContainer` | Low-contrast neutral surface in the secondary family. Subtle backgrounds that need to separate from the page without implying brand meaning: tonal chip fills, quiet badges, muted selection backgrounds, segmented-control tracks, secondary button fills. Resolves to `ref.palette.neutral.100` (light) / `ref.palette.neutral.600` (dark). The dark step sits two bands lighter than a strict mirror would land — at `neutral.800` the secondary fill would collide with every `surfaceContainer*` and `surfaceVariant` (all `neutral.800` in dark); at `neutral.700` it would still collide with `surfaceContainerHighest` (the topmost surface band). `neutral.600` lifts the secondary accent one step clear of the entire surface ladder so a secondary fill placed on any host — including the most lifted overlay surfaces — stays distinct, while remaining inside the muted band. |
|
|
158
|
-
| `color.onSecondaryContainer` | Foreground for content placed on `secondaryContainer`. Resolves to `ref.palette.neutral.900` (light) / `ref.palette.neutral.100` (dark). |
|
|
159
|
-
|
|
160
|
-
###### Brand
|
|
161
|
-
|
|
162
|
-
| Token | Role |
|
|
163
|
-
|--------------------------|-------------------------------------------------------------------------------|
|
|
164
|
-
| `color.brand` | The product's signature red — a high-attention accent reserved for notification counts, unread badges, eyebrow flags, and brand-identity moments (logomark fills, brand-tagged callouts). One tonal step **brighter** than `error` in both modes (`red.500` brand vs. `red.600` / `red.700` error), so the two reds stay visually distinct on the same surface: brand reads as energetic identity, error reads as a deeper destructive signal. Resolves to `ref.palette.red.500` in **both** light and dark modes — brand identity stays stable across themes, and the 500 step is the brightest red the palette ships that still clears AA against `onBrand` (`neutral.50`) for white-on-brand labels. |
|
|
165
|
-
| `color.onBrand` | Foreground placed on top of `brand`. Label text and icons inside brand-filled surfaces (notification counts, brand badges). Resolves to `ref.palette.neutral.50`. White-on-`red.500` lands at ~4.7:1 — clears AA for normal text in both modes. |
|
|
166
|
-
| `color.brandContainer` | Low-chroma tinted surface in the brand family. Soft brand callouts, "what's new" banners, promotional tiles, marketing surfaces where the energy of `brand` would overwhelm. Resolves to `ref.palette.red.50` (light) / `ref.palette.red.900` (dark). Light is one step lighter than `errorContainer` (`red.50` vs. `red.100`) so the brand callout reads as a quiet identity touch rather than a warning. |
|
|
167
|
-
| `color.onBrandContainer` | Foreground for content placed on `brandContainer`. Resolves to `ref.palette.red.600` (light) / `ref.palette.red.400` (dark) — both stay in the saturated red family so the foreground reads as *red on tinted red*, not as *near-black on tinted red*. The dark step lifts one band higher than light's mirror would suggest because identical luminance gaps read darker on dark surfaces. |
|
|
168
|
-
|
|
169
|
-
###### Success
|
|
170
|
-
|
|
171
|
-
| Token | Role |
|
|
172
|
-
|-----------------------------|-------------------------------------------------------------------------------|
|
|
173
|
-
| `color.success` | The signal color for positive confirmation — completed states, success toasts, "saved" pills, validated form fields, healthy status indicators. Reserved strictly for affirmative outcomes; decorative use erodes its signaling power. Resolves to `ref.palette.green.500` in **both** light and dark modes — mirrors `brand`'s cross-mode stability so the success signal reads as the same green across themes, and the 500 step is the brightest green the palette ships that still clears AA against `onSuccess` (`neutral.50`) for white-on-success labels. |
|
|
174
|
-
| `color.onSuccess` | Foreground placed on top of `success`. Label text and icons inside success-filled surfaces. Resolves to `ref.palette.neutral.50`. |
|
|
175
|
-
| `color.successContainer` | Low-chroma tinted surface in the success family. Soft success callouts, "you're all set" banners, completed-task tiles where `success` would overwhelm. Resolves to `ref.palette.green.50` (light) / `ref.palette.green.900` (dark) — mirrors `brandContainer`'s shallow-light / deep-dark structure. |
|
|
176
|
-
| `color.onSuccessContainer` | Foreground for content placed on `successContainer`. Resolves to `ref.palette.green.600` (light) / `ref.palette.green.400` (dark) — both stay in the saturated green family so the foreground reads as *green on tinted green*, not as *near-black on tinted green*. The dark step lifts one band higher than light's mirror would suggest because identical luminance gaps read darker on dark surfaces. |
|
|
177
|
-
|
|
178
|
-
###### Error
|
|
179
|
-
|
|
180
|
-
| Token | Role |
|
|
181
|
-
|--------------------------|-------------------------------------------------------------------------------|
|
|
182
|
-
| `color.error` | The signal color for destructive and error states. Destructive CTAs (Delete, Remove), form-field error outlines, critical status indicators, alert icons. Reserved strictly for negative or dangerous meaning — decorative use erodes its signaling power. One tonal step **darker** than `brand` (red.500) in both modes so destructive moments sit deeper and graver than brand-identity moments on the same screen. Resolves to `ref.palette.red.600` (light) / `ref.palette.red.700` (dark). |
|
|
183
|
-
| `color.onError` | Foreground placed on top of `error`. Label text and icons inside error-filled surfaces. Resolves to `ref.palette.neutral.50`. |
|
|
184
|
-
| `color.errorContainer` | Low-chroma tinted surface in the error family. Inline error message backgrounds, warning banners, failed-state tiles. Less alarming than `error`, so appropriate for larger areas. Resolves to `ref.palette.red.100` (light) / `ref.palette.red.900` (dark). |
|
|
185
|
-
| `color.onErrorContainer` | Foreground for content placed on `errorContainer`. Resolves to `ref.palette.red.700` (light) / `ref.palette.red.500` (dark). |
|
|
186
|
-
|
|
187
|
-
#### Surface stack
|
|
188
|
-
|
|
189
|
-
Page background and the elevation-tier container surfaces.
|
|
190
|
-
|
|
191
|
-
##### Three sub-groups
|
|
192
|
-
|
|
193
|
-
1. **Base canvas** (`surface` / `onSurface`) — the foundation everything else sits on.
|
|
194
|
-
2. **Canvas modifiers** (`surfaceVariant` / `onSurfaceVariant` / `surfaceDim` / `surfaceBright`) — alternate base tones for quiet separation, recess, or spotlight. `surfaceVariant` carries its own paired foreground for two-tier text hierarchy; `Dim` / `Bright` keep `onSurface` as foreground.
|
|
195
|
-
3. **Container ladder** (`surfaceContainerLowest` → `Low` → `default` → `High` → `Highest`) — five ordered tiers indicating *spatial role* (sunken → recessed → default → raised → topmost). In light mode the tones collapse onto `#ffffff` by design; lift comes from `elevation.*` shadows.
|
|
251
|
+
1. **State-overlay formula** — `overlay.hover` / `overlay.pressed` composited over a surface, per [State overlays](#state-overlays).
|
|
252
|
+
2. **Decorative gradient atmospherics** — an accent-toned stop fading to `transparent` inside a gradient over a flat `surface` base, where the *base under the gradient* governs text contrast. The gradient is decoration, not a content surface.
|
|
196
253
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
###### Base & variants
|
|
200
|
-
|
|
201
|
-
| Token | Role |
|
|
202
|
-
|--------------------------|----------------------------------------------------------------------------------------------|
|
|
203
|
-
| `color.surface` | The base page background — the canvas everything else sits on. Root app background, empty regions, any large flat area that should read as the "ground" of the UI. Resolves to `ref.palette.neutral.0` (light) / `ref.palette.neutral.900` (dark). |
|
|
204
|
-
| `color.onSurface` | Primary foreground against `surface`. Body copy, headings, primary icons. Clears WCAG AA against every `surface*` token. Resolves to `ref.palette.neutral.900` (light) / `ref.palette.neutral.50` (dark). |
|
|
205
|
-
| `color.surfaceVariant` | A quiet alternate surface tone — and the **canonical fill for any "subtle container" region** that should read as separated from the page without being lifted. Input field fills, disabled control backgrounds, zebra-striping, muted section backgrounds, and contained sub-modules inside a card (poll banners, citation bodies, attachment chips). Prefer `surfaceVariant` over `surfaceContainer*` for this role: `surfaceContainer` collapses onto `surface` in light mode by design and produces no visible separation when used as a sub-module fill. Resolves to `ref.palette.neutral.100` (light) / `ref.palette.neutral.800` (dark). |
|
|
206
|
-
| `color.onSurfaceVariant` | Secondary foreground for lower-emphasis text on any surface tone. Supporting copy, placeholders, helper text, metadata, inactive icon fills. Deliberately lighter than `onSurface` to establish a two-tier text hierarchy. Resolves to `ref.palette.neutral.700` (light) / `ref.palette.neutral.300` (dark). |
|
|
207
|
-
| `color.surfaceDim` | A darker variant of `surface` — the darkest base background. Use when the page behind an elevated element needs to recede (dimmed canvas behind a modal/drawer, "quiet" screens where raised cards carry the focus). Resolves to `ref.palette.neutral.200` (light) / `ref.palette.neutral.900` (dark). |
|
|
208
|
-
| `color.surfaceBright` | A brighter variant of `surface` — the brightest base background. Spotlight moments where the base layer itself should feel elevated (hero regions, focus screens, brightened split-view panels). Resolves to `ref.palette.neutral.0` (light) / `ref.palette.neutral.800` (dark). |
|
|
209
|
-
|
|
210
|
-
###### Container ladder
|
|
211
|
-
|
|
212
|
-
| Token | Role |
|
|
213
|
-
|---------------------------------|----------------------------------------------------------------------------------------------|
|
|
214
|
-
| `color.surfaceContainerLowest` | Elevation level 0 — the lowest tier in the container stack. The most recessed tone: a soft notch beneath `surface` in light mode, the true palette floor (pure black) in dark mode. Sunken or inset regions: input wells, disabled control bodies, trough/rail backgrounds, page-header recessed bands. Resolves to `ref.palette.neutral.100` (light) / `ref.palette.neutral.1000` (dark). |
|
|
215
|
-
| `color.surfaceContainerLow` | Elevation level 1 — low-prominence containers. Backgrounded secondary panels, sidebar sections, cards that should feel "attached" to the page rather than floating. Less recessed than `surfaceContainerLowest`. Resolves to `ref.palette.neutral.100` (light) / `ref.palette.neutral.900` (dark). |
|
|
216
|
-
| `color.surfaceContainer` | Elevation level 2 — the default container tone. Standard cards, list items, feed tiles, most everyday content surfaces. Start here when in doubt. In light mode this matches `surface`; in dark mode it is one tonal step above `surface`. Resolves to `ref.palette.neutral.0` (light) / `ref.palette.neutral.800` (dark). |
|
|
217
|
-
| `color.surfaceContainerHigh` | Elevation level 3 — the "raised" tone. Two families share this fill: (a) **scrim-anchored interruptions** — modals and dialogs, search view, bottom sheets, expanded navigation drawers; and (b) **in-page raised chrome** — bottom app bar, FAB surface variant, filter / toolbar button bodies (and the chip-chrome tabs that inherit them), selected cards, nested emphasized sections, neutral placeholder fills. Note: Toggle Button's active state uses `transparent` (not `surfaceContainerHigh`) so it sits cleanly on any host tier; only the inactive state's `primary` fill or the filter/toolbar chrome sit on this token. Tonally identical to `surfaceContainer` in both modes — visible lift comes from `elevation.overlay` (for scrim-anchored surfaces) or `elevation.floating` (for in-page raised containers), not an ever-brightening fill. Resolves to `ref.palette.neutral.0` (light) / `ref.palette.neutral.800` (dark). |
|
|
218
|
-
| `color.surfaceContainerHighest` | Elevation level 4 — the topmost container tone, reserved for the *most* lifted surfaces that float over everything else without their own scrim. Menus, tooltips, popovers, filled text-field bodies, search bars. In light mode matches `surfaceContainer`; in dark mode steps up one tier so the topmost layer reads against the stack beneath it, reinforced by `elevation.overlay`. Resolves to `ref.palette.neutral.0` (light) / `ref.palette.neutral.700` (dark). |
|
|
219
|
-
|
|
220
|
-
**Tonal elevation is capped, not stacked.** The Chorus brand goal is *calm and trustworthy*; ever-brightening surfaces feel showy and break the calm. Lift is expressed by `elevation.*` shadows; the surface names carry semantic weight ("this is a modal") even when the tone is identical to a card.
|
|
221
|
-
|
|
222
|
-
#### Outline · Inverse · Focus · Scrim
|
|
223
|
-
|
|
224
|
-
Borders, inverted overlays, focus indicators, backdrop dimming.
|
|
225
|
-
|
|
226
|
-
##### Five role-clusters
|
|
227
|
-
|
|
228
|
-
- **Outline cluster** (`outline` / `outlineVariant`) — high vs. low emphasis border pair.
|
|
229
|
-
- **Inverse cluster** (`inverseSurface` / `inverseOnSurface`) — mini-stack for elements that must contrast with the page (snackbars, tooltips). Action accents inside inverted components fall back to the regular `primary` family — the inverse canvas is contrast-tuned to clear AA against `primary` without a dedicated step.
|
|
230
|
-
- **Focus cluster** (`focus` / `focusInset`) — outer ring + inner counter-ring pair. Always composed together (see [Focus ring composition](#focus-ring-composition)).
|
|
231
|
-
- **Scrim** (solo) — translucent black for backdrop dimming.
|
|
232
|
-
- **Elevation ink** (solo) — base shadow color, referenced only from `elevation.*` definitions, never as a fill.
|
|
233
|
-
|
|
234
|
-
###### Outline
|
|
235
|
-
|
|
236
|
-
| Token | Role |
|
|
237
|
-
|------------------------|------------------------------------------------------------------------------------------------------|
|
|
238
|
-
| `color.outline` | High-emphasis border. Outlined buttons, form field borders, selected-state strokes, dividers that need to carry visual weight. Resolves to `ref.palette.neutral.400` (light) / `ref.palette.neutral.500` (dark). |
|
|
239
|
-
| `color.outlineVariant` | Low-emphasis border. Subtle dividers, table row separators, card edges, decorative hairlines where `outline` would be too loud. Resolves to `ref.palette.neutral.200` (light) / `ref.palette.neutral.700` (dark). The dark step sits one band higher than a strict mirror of the light step (`neutral.200`) would land — at `neutral.800` the divider would collide with `surfaceVariant` (`neutral.800` in dark) and vanish against any sub-module fill. `neutral.700` keeps a low-emphasis divider visible against both `surface` (`neutral.900`) and `surfaceVariant` (`neutral.800`) without escalating to the high-emphasis `outline`. |
|
|
240
|
-
|
|
241
|
-
###### Inverse
|
|
242
|
-
|
|
243
|
-
| Token | Role |
|
|
244
|
-
|--------------------------|------------------------------------------------------------------------------------------------------|
|
|
245
|
-
| `color.inverseSurface` | A surface that deliberately reverses the current mode — dark in light mode, light in dark mode. Components that must visually contrast with the surrounding page to grab attention: snackbars, toast backgrounds, coach-mark tooltips, onboarding highlights. Resolves to `ref.palette.neutral.900` (light) / `ref.palette.neutral.50` (dark). |
|
|
246
|
-
| `color.inverseOnSurface` | Foreground on `inverseSurface`. Text and icons inside inverted surfaces. Resolves to `ref.palette.neutral.50` (light) / `ref.palette.neutral.900` (dark). |
|
|
247
|
-
|
|
248
|
-
###### Focus
|
|
249
|
-
|
|
250
|
-
| Token | Role |
|
|
251
|
-
|--------------------|------------------------------------------------------------------------------------------------------|
|
|
252
|
-
| `color.focus` | Outer focus-ring color. Intentionally inverse-toned — dark in light mode, light in dark mode — so the ring reads against any surface in the stack regardless of the control's own fill. See [Focus ring composition](#focus-ring-composition) for the full three-layer rule. Resolves to `ref.palette.black.1000` (light) / `ref.palette.white.1000` (dark). |
|
|
253
|
-
| `color.focusInset` | Inner counter-ring paired with `focus`. Mirrors `focus` in the opposite direction so even when the outer ring meets a similarly-toned background, the inset edge keeps the indicator legible. Resolves to `ref.palette.white.1000` (light) / `ref.palette.black.1000` (dark). |
|
|
254
|
-
|
|
255
|
-
###### Overlay
|
|
254
|
+
#### Dark-mode strategy
|
|
256
255
|
|
|
257
|
-
|
|
258
|
-
|-------------------|------------------------------------------------------------------------------------------------------|
|
|
259
|
-
| `color.scrim` | Translucent black used to dim content behind a raised overlay. The backdrop behind modals, drawers, menus, and bottom sheets — focuses attention on the foreground and blocks interaction with the obscured layer. Resolves to `ref.palette.black.800`. |
|
|
260
|
-
| `color.elevation` | Base color used to build elevation shadows (composited with opacity inside `elevation.*` definitions). Not for fills — reference only from elevation definitions. Resolves to `ref.palette.black.1000`. |
|
|
261
|
-
| `color.placeholderImage.start` / `color.placeholderImage.end` | Gradient endpoints for a decorative **image-placeholder fallback** — composed as `linear-gradient(135deg, start 0%, end 100%)` on slots that expose user-supplied imagery (Feed post thumbnail, Citation hero) when the image `src` is empty or unloaded. Theme-aware: cool teal in light (`#c8e0e1 → #2d6f72`), cool neutral in dark (`#3a4548 → #1a2226`) so the placeholder reads as "image content here" against either surface ladder without competing with adjacent thumbnails. **Not for non-image fills** — for empty/skeleton states of solid surfaces, use `surfaceContainerHigh` instead. |
|
|
256
|
+
Each role carries its own light and dark value; four inversion rules apply across the families.
|
|
262
257
|
|
|
263
|
-
|
|
258
|
+
- **Neutral roles invert along the ramp.** Text, icon, border, surface, and neutral backgrounds swap ends of the grey ladder — light surfaces go dark, dark text goes light (`surface.default` `neutral.0` → `neutral.1000`; `text.default` `neutral.1000` → `neutral.100`).
|
|
259
|
+
- **Status & accent hues hold or brighten — they don't invert.** `text.brand` stays `brand.red` in both modes so identity is stable; `text.danger` brightens for a dark page (`red.500` → `red.400`); `text.link` / `text.success` / `border.primary` dim one step (`blue.500` → `blue.400`) so the blue reads without glaring. On-fill roles stay light in both modes.
|
|
260
|
+
- **Function backgrounds flip depth, not hue.** Pale banner tints in light become deep tints in dark (`background.selected` `blue.50` → `blue.900`; `background.danger` `red.100` → `red.900`), keeping the paired status foreground legible on either page.
|
|
261
|
+
- **Overlays composite over whatever is beneath.** The State layers (`overlay.hover` / `overlay.pressed`) hold the same low alpha in both modes; the Scrim layers deepen in dark so the backdrop still separates from the darker page (`overlay.scrim` ~32% → ~75%).
|
|
264
262
|
|
|
265
|
-
- **Chromatic accents (`primary`, `brand`, `success`, `error`) do NOT invert their on-pair between modes.** `error` nudges one tonal step in dark (`red.600` → `red.700`) so the fill still reads against a dark page; `primary` stays at `blue.500`, `brand` at `red.500`, `success` at `green.500` in both modes because each hue clears AA against both `surface` tones and its `on*` foreground (`neutral.50`) without a nudge. The `on*` foreground stays at `neutral.50` across all four. Keeps brand identity stable across modes.
|
|
266
|
-
- **Neutral roles (`secondary`, `surface*`, `onSurface*`, `outline*`) invert as usual.**
|
|
267
|
-
- **Container pairs (`primaryContainer` / `onPrimaryContainer`, etc.) flip the *container*, not the foreground family**: light mode container is shallow (e.g. `blue.50` for primary, `red.50` / `green.50` for brand / success, `red.100` for error) with a saturated mid-band foreground (`blue.600`); dark mode container goes deep (`blue.900`) with a brighter mid-band foreground (`blue.400`). Both modes keep the foreground in the saturated primary family so the pair reads as *blue on tinted blue*, not *near-black on tinted blue*. Primary's light container sits one step brighter than the other quartets because it appears most often (active nav rows, brand callouts). The dark foreground lifts one band higher than a strict mirror (`blue.400` instead of `blue.500`) because equal luminance gaps appear shallower on dark surfaces.
|
|
268
|
-
- **Focus ring inverts** so the outer ring is always inverse-toned to the page.
|
|
269
263
|
|
|
270
264
|
#### Data visualization palette
|
|
271
265
|
|
|
@@ -283,13 +277,13 @@ Charts and category-coded surfaces draw from the same six reference hues, organi
|
|
|
283
277
|
|
|
284
278
|
- **Six maximum for categorical.** Past six categories, group the long tail into "Other" or add a secondary visual channel (texture, position).
|
|
285
279
|
- **Brand color comes first only when it carries meaning.** Using `blue.500` for the first series implies that series is "primary." If categories are equal, rotate the order or pick a non-brand starting hue.
|
|
286
|
-
- **Reuse `
|
|
280
|
+
- **Reuse the `danger` and `success` roles for negative / positive coding** — don't introduce a chart-specific red. Note `success` resolves to blue, not green; when a chart needs a true green for positive coding, reach for `ref.palette.green.500` directly (dataviz-scoped exception).
|
|
287
281
|
- **Dark mode shifts the steps, not the hues.** Light uses `*.500` for categorical; dark uses `*.400`/`*.300` so contrast against the dark canvas holds.
|
|
288
282
|
- **Pair with a non-color channel** — pattern fills, direct labels, or shape differentiation. ~4% of users cannot distinguish red from green.
|
|
289
283
|
|
|
290
284
|
### Typography
|
|
291
285
|
|
|
292
|
-
One typeface (Pretendard) handles both Latin and Hangul, materialized as
|
|
286
|
+
One typeface (Pretendard) handles both Latin and Hangul, materialized as 14 roles across four purpose categories × three sizes (`body` and `label` add a fourth `xs` rung) — weight and line-height carry meaning by purpose, not by size.
|
|
293
287
|
|
|
294
288
|
#### Font family
|
|
295
289
|
|
|
@@ -297,7 +291,7 @@ The system typeface is **Pretendard**.
|
|
|
297
291
|
|
|
298
292
|
**Why Pretendard** — Chorus is a community service with high text volume where Hangul (국문) and Latin (영문) routinely appear side by side. Most sans-serif families are tuned for one script and break when scripts mix. Pretendard's Hangul and Latin share compatible x-height, weight, and optical rhythm, so mixed-script lines read as one continuous texture.
|
|
299
293
|
|
|
300
|
-
**How to apply** — one family for every surface (display, heading, body, label
|
|
294
|
+
**How to apply** — one family for every surface (display, heading, body, label). Do not substitute a different family for Latin-only or Korean-only regions. The stack falls back to platform system fonts only when Pretendard fails to load:
|
|
301
295
|
|
|
302
296
|
```
|
|
303
297
|
Pretendard, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif
|
|
@@ -305,12 +299,12 @@ Pretendard, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Ne
|
|
|
305
299
|
|
|
306
300
|
#### Categories × Sizes
|
|
307
301
|
|
|
308
|
-
Four purpose categories × three size levels, plus
|
|
302
|
+
Four purpose categories × three size levels, plus two extra `xs` rungs — `body.xs` (12px reading metadata) and `label.xs` (the caption-scale control role, 10px) = 14 type roles, each composed of four atomic properties.
|
|
309
303
|
|
|
310
|
-
- **Category axis (purpose)** — `display` (hero) → `heading` (structural title) → `body` (reading) → `label` (control
|
|
311
|
-
- **Size axis (emphasis within a category)** — `lg` / `md` / `sm`. `md` is the default.
|
|
304
|
+
- **Category axis (purpose)** — `display` (hero) → `heading` (structural title) → `body` (reading, incl. `body.xs` for inline metadata) → `label` (control, incl. the caption-scale `label.xs`). Position determines weight and line-height by purpose, not by size.
|
|
305
|
+
- **Size axis (emphasis within a category)** — `lg` / `md` / `sm`; `body` and `label` add `xs`. `md` is the default.
|
|
312
306
|
- **Composition** — each grid cell composes four reference-tier values: `size` / `weight` / `line` / `tracking`. Never mix-and-match across cells.
|
|
313
|
-
- **Responsive scaling grows with hierarchy, above the `md` baseline only.** `display.lg` jumps two scale steps on web (≥800px); `heading.lg` jumps one. `display.md/sm` / `heading.md/sm` stay constant. Body / label
|
|
307
|
+
- **Responsive scaling grows with hierarchy, above the `md` baseline only.** `display.lg` jumps two scale steps on web (≥800px); `heading.lg` jumps one. `display.md/sm` / `heading.md/sm` stay constant. Body / label are unconditionally flat. **`md` is the baseline, only sizes above it grow on web.**
|
|
314
308
|
|
|
315
309
|
| Role | Size (mobile → Web) | Weight | Line | Tracking | Use |
|
|
316
310
|
|---------------|---------------------|------------|--------|---------------|----------------------------------------------------|
|
|
@@ -318,21 +312,22 @@ Four purpose categories × three size levels, plus the single rung-less `caption
|
|
|
318
312
|
| `typo.display.md` | 40 px | 700 Bold | 1.25 tight | -0.02em tight | Section heroes, featured content callouts, high-impact empty states. |
|
|
319
313
|
| `typo.display.sm` | 32 px | 700 Bold | 1.25 tight | -0.02em tight | Compact heroes and sub-hero banners where vertical space is constrained. |
|
|
320
314
|
| `typo.heading.lg` | 24 → 32 px | 600 Semibold | 1.25 tight | -0.01em snug | Page-level title — the single top heading of a screen or dialog. |
|
|
321
|
-
| `typo.heading.md` |
|
|
315
|
+
| `typo.heading.md` | 18 px | 600 Semibold | 1.25 tight | -0.01em snug | Section title — card titles, modal titles, settings groups. |
|
|
322
316
|
| `typo.heading.sm` | 16 px | 600 Semibold | 1.25 tight | -0.01em snug | Sub-section title, list-group headers, inline emphasis headings. |
|
|
323
|
-
| `typo.body.lg` |
|
|
317
|
+
| `typo.body.lg` | 18 px | 400 Regular | 1.5 normal | 0em normal | Long-form bodies — article text, descriptions, main readable content. |
|
|
324
318
|
| `typo.body.md` | 16 px | 400 Regular | 1.5 normal | 0em normal | Default body. Single-topic page bodies (article text, one-up content surfaces, descriptions where the reader stays in the block) and form-field input values. See [Composition recipes § Body text size](#body-text-size-14px-in-mixed-group-sections-16px-on-single-topic-pages) for picking between `body.md` and `body.sm`. |
|
|
325
|
-
| `typo.body.sm` | 14 px | 400 Regular | 1.
|
|
319
|
+
| `typo.body.sm` | 14 px | 400 Regular | 1.35 snug | 0em normal | Compact body. The right pick when a section composes multiple distinct text groups (cards listing several short descriptions, settings rows, feed items, dialog/callout messages); also dense lists, secondary descriptions, inline helper prose. Still a reading size, not a caption. |
|
|
320
|
+
| `typo.body.xs` | 12 px | 400 Regular | 1.35 snug | 0em normal | Inline supplementary metadata that rides alongside reading content — timestamps, company / author attribution, nicknames, counts. The reading ramp's smallest rung; stays 400 to read as content, not a control. Pair with `label.sm` (600) when a metadata cluster needs an identifying anchor. |
|
|
326
321
|
| `typo.label.lg` | 16 px | 600 Semibold | 1.5 normal | 0em normal | Primary CTA buttons, prominent tab labels, standalone form labels. |
|
|
327
322
|
| `typo.label.md` | 14 px | 600 Semibold | 1.5 normal | 0em normal | Default control label. Standard buttons, input labels, menu items, chip labels; also form helper text, footnote-style explanations, figure captions. |
|
|
328
|
-
| `typo.label.sm` | 12 px | 600 Semibold | 1.5 normal | 0.02em wide | Compact controls — dense toolbars, small badges, inline tag labels, supporting actions; also
|
|
329
|
-
| `typo.
|
|
323
|
+
| `typo.label.sm` | 12 px | 600 Semibold | 1.5 normal | 0.02em wide | Compact controls — dense toolbars, small badges, inline tag labels, supporting actions; also *identifying* metadata that must stay scannable (channel / author names, follow state). Plain supplementary metadata — timestamps, company, nicknames, counts — uses `body.xs` (400), not this rung. |
|
|
324
|
+
| `typo.label.xs` | 10 px | 600 Semibold | 1.5 normal | 0.02em wide | Smallest label rung (caption-scale) — badge counts, legal fine print, dense metadata columns, tab-bar labels. Use sparingly. |
|
|
330
325
|
|
|
331
326
|
#### Tracking & line-height principles
|
|
332
327
|
|
|
333
|
-
- **Bold display, semibold heading, semibold label
|
|
328
|
+
- **Bold display, semibold heading, semibold label, regular body.** Weight differentiates roles at the same size — `body.md` (400) and `label.lg` (600) share 16px but read differently. The split repeats wherever sizes coincide: at 12px, `label.sm` (600) reads as *identifying* metadata (channel name, follow state) while `body.xs` (400) reads as *supplementary* metadata (timestamp, nickname, count) — weight, not size, says which you're reading. The split tracks *emphasis*, not interactivity: a tappable nickname still sits in `body.xs`, because a feed need not elevate it; `label.sm`'s 600 is spent only where the screen genuinely wants attention drawn (channel name, follow control). Labels borrow heading's 600 to read as scannable; body stays regular for comfortable reading.
|
|
334
329
|
- **Tracking only diverges at the extremes.** Display compresses (`-0.02em`); small UI text and uppercase widens (`0.02em`). Body and label-md use the typeface's intended spacing (`0em`).
|
|
335
|
-
- **Line-height splits by purpose
|
|
330
|
+
- **Line-height splits by purpose and stays a unitless ratio so it scales with the font.** Display and heading use `tight` (1.25); long-form reading (`body.lg` / `body.md`) and all controls (`label.*`) use `normal` (1.5); the small reading rungs `body.sm` (14px) and `body.xs` (12px) use `snug` (1.35) — tighter than long-form because compact metadata sits in short runs. Line-height is never pinned to an absolute px: keeping it unitless is what lets the line grow with the glyph when font-size scales to the user's font-size preference. A ratio may land on a half-pixel at the 16px root (e.g. `heading.md` 18 × 1.25 = 22.5) — that is intended; once text scales, exact pixels are moot.
|
|
336
331
|
|
|
337
332
|
#### Letter-spacing scale
|
|
338
333
|
|
|
@@ -350,10 +345,24 @@ Mapping into `typo.*`: display → `tight`, heading → `snug`, body / reading
|
|
|
350
345
|
|
|
351
346
|
#### Font-size scale
|
|
352
347
|
|
|
353
|
-
Built on the 8px base (`fontSize.100` = 8px = 1×, `fontSize.200` = 16px = 2×), with finer in-between rungs (10 / 14 / 18 / 56 / 72px) added where legibility demands resolution the spacing scale cannot provide. Each rung carries `$rem` alongside `$value
|
|
348
|
+
Built on the 8px base (`fontSize.100` = 8px = 1×, `fontSize.200` = 16px = 2×), with finer in-between rungs (10 / 14 / 18 / 56 / 72px) added where legibility demands resolution the spacing scale cannot provide. Each rung carries `$rem` alongside `$value`, and **CSS compiles font-size to `rem`** — the accessibility-recommended unit, which respects the user's browser / OS font-size preference so text scales — while spacing, radius, and other dimensions stay `px` (fixed structure: only type scales, the layout grid does not).
|
|
349
|
+
|
|
350
|
+
**Base font size: `1rem = 16px`.** This is the browser default — the user's *100% / default* font size — **not a fixed equation**: when the user raises their font-size preference, `1rem` grows past 16px and every type token scales with it. Two consequences:
|
|
351
|
+
|
|
352
|
+
- **Don't pin the root.** For the scaling to work, a consuming web surface must **not** hard-set the root (`html`) font-size to a px value — `html { font-size: 16px }` pins the root and silently disables the user's preference. Leave it at the browser default (or `font-size: 100%`).
|
|
353
|
+
- **The px column is the design reference, not the shipped unit.** The pixel values in the tables above are the *rendered size at the 16px base* — what Figma shows and what designers spec — while the shipped web unit is `rem`. **Native ports never use `rem`**: they read the px `$value` and emit `sp` (Android) / points + Dynamic Type (iOS), which scale through the OS, so the `1rem = 16px` convention is web-only.
|
|
354
354
|
|
|
355
355
|
The reference ladder is *material*, not vocabulary: `typo.*` categories pick rungs from it; product code never references `fontSize.*` directly.
|
|
356
356
|
|
|
357
|
+
#### Dynamic Type & the display/heading cap
|
|
358
|
+
|
|
359
|
+
Type scales to the user's font-size preference on every platform — web `rem`, iOS Dynamic Type, Android `sp` — all anchored to the same `1rem = 16px` base. Two policies keep that scaling consistent and layout-safe across platforms:
|
|
360
|
+
|
|
361
|
+
- **Reading roles scale uncapped; `display` and `heading` cap at 1.6×.** `body` and `label` — what a low-vision reader actually reads — honor the user's setting all the way up. `display` and `heading` are large, short, and mostly decorative, so their growth is bounded at **1.6× the base size**: an uncapped extreme setting would push `display.lg` (48px) past ~120px and break layouts, while 1.6× still gives a strong jump (`heading.lg` 24→38px). The bound is a single cross-platform policy, not a per-port accident.
|
|
362
|
+
- **Line-height stays a unitless ratio under scaling.** The 1.25 / 1.35 / 1.5 ratios grow with the glyph rather than freezing to an absolute px, so line spacing stays proportional as type scales — on every platform.
|
|
363
|
+
|
|
364
|
+
*Implementation status.* The 1.6× cap ships on iOS (`ChorusText.headingMaxScale`) and web (`min(rem, 1.6×px)` on `display` / `heading` in `tokens.css`); the Android per-category cap is the remaining follow-up. Line-height now applies on all three platforms — web (unitless), Android (baked `size × ratio` in `sp`), and iOS (`.lineSpacing` from the token ratio). Chrome with fixed geometry (tab bar, chip) still needs a layout pass so its labels don't clip at the largest accessibility sizes.
|
|
365
|
+
|
|
357
366
|
#### Casing
|
|
358
367
|
|
|
359
368
|
**Sentence case is the default** for every piece of UI text — navigation items, section titles, button and label text, page titles, dialog and toast bodies. Capitalize the first word and proper nouns; everything else stays lowercase. **Title Case Like This is not used anywhere in product surfaces.**
|
|
@@ -362,7 +371,7 @@ The reference ladder is *material*, not vocabulary: `typo.*` categories pick run
|
|
|
362
371
|
|
|
363
372
|
**UPPERCASE is reserved for in-content category markers** — eyebrows, overlines, table-section headers — pairing with `letterSpacing.wider` (see [Letter-spacing scale](#letter-spacing-scale)). It does **not** apply to navigation structure (side-nav group labels, page-nav section headers); those use sentence case with hierarchy expressed by font size and weight. Apply via CSS `text-transform: uppercase`, never by writing the text in caps in source — the underlying text stays sentence case so it reads correctly when the transform is removed (search, screen readers, diff review, localization).
|
|
364
373
|
|
|
365
|
-
**Exceptions** — proper nouns (Pretendard, Hangul), product names, code identifiers (`sys.color.
|
|
374
|
+
**Exceptions** — proper nouns (Pretendard, Hangul), product names, code identifiers (`sys.color.text.default`), acronyms (CTA, AA, WCAG) keep their natural casing.
|
|
366
375
|
|
|
367
376
|
##### Segmented sentence case
|
|
368
377
|
|
|
@@ -409,10 +418,10 @@ The grid is **drawing-area, not bounding-box**. A 16px icon should occupy ~13px
|
|
|
409
418
|
|
|
410
419
|
Icons inherit the same `on*` foreground tokens as the text they sit with by default. The dedicated `sys.color.icon.*` palette is the only other place a glyph may pick a colour from — components must never reach into `ref.palette.*` to paint an icon.
|
|
411
420
|
|
|
412
|
-
- **Solo icon (icon-only button)** — color is the parent control's foreground role (`
|
|
421
|
+
- **Solo icon (icon-only button)** — color is the parent control's foreground role (`icon.default` for a ghost button, `icon.onFill` inside a primary fill).
|
|
413
422
|
- **Icon + label** — both use the same foreground; never paint the icon a different hue for emphasis. Hierarchy belongs to the label.
|
|
414
423
|
- **Inactive / disabled** — inherit from the surrounding `state.disabled` opacity; do not pre-darken the icon SVG.
|
|
415
|
-
- **Status icons** (success/error checks, alert glyphs) follow the
|
|
424
|
+
- **Status icons** (success/error checks, alert glyphs) follow the role they signal: `icon.success`, `icon.danger`, or the paired `text.onFill` / `icon.onFill` when on a filled status surface.
|
|
416
425
|
- **Semantic-glyph paint** — when the glyph itself carries meaning that is *not* a backgrounded status pair (an unpressed vs. starred favourite, a "live" pulse next to text, a premium/AI badge glyph), pick from `sys.color.icon.*` (`muted`, `yellow`, `red`, `blue`, `green`, `purple`). These hues are tuned one step lighter than their `sys.color.*` background analogues for small-glyph optical readability and pair with `icon.muted` (translucent inverse-tone) for the inactive state on the same affordance — flip state by colour, not by shape.
|
|
417
426
|
|
|
418
427
|
| Token | Light | Dark | Typical use |
|
|
@@ -468,7 +477,7 @@ The ladder is unitless. Scales bind it to different units:
|
|
|
468
477
|
|
|
469
478
|
- **Spacing** binds to **pixels** — each rung is a `space.*` step (`0px` … `80px`); see [Reference scale](#reference-scale).
|
|
470
479
|
- **Type rungs** bind to **pixels** for ladder rungs (8 / 16 / 24 / 32 / 40 / 48 / 64 / 80px); finer typographic resolution adds in-between rungs where legibility demands it ([Font-size scale](#font-size-scale)).
|
|
471
|
-
- **Opacity** binds to **percent** — `palette.black.*` / `palette.white.*` alphas are drawn from the ladder read as percent, plus a color-specific endpoint at `100%` for fully-opaque overlays
|
|
480
|
+
- **Opacity** binds to **percent** — `palette.black.*` / `palette.white.*` alphas are drawn from the ladder read as percent, plus a color-specific endpoint at `100%` for fully-opaque overlays and shadow ink. 100 isn't on the spacing/typography ladder because spacing has no "fully opaque." See [Opacity ramp](#opacity-ramp).
|
|
472
481
|
|
|
473
482
|
Surfaces needing a value outside the ladder are a code-review signal — either the ladder needs a step, or the surface is reaching past the system tier.
|
|
474
483
|
|
|
@@ -681,7 +690,7 @@ Hardcoded values (`border: 1px`, `border: 2px`) accumulate inconsistently across
|
|
|
681
690
|
| `borderWidth.thin` | 2px | Emphasis borders — focus ring outer, selected-state outlines, error-state field borders. Strong enough to register without competing with the fill. |
|
|
682
691
|
| `borderWidth.thick` | 4px | Load-bearing strokes — keyboard-focus emphasis on touch surfaces, status indicators (active tab underline at hero scale), decorative rules. Use sparingly. |
|
|
683
692
|
|
|
684
|
-
The focus ring composition (see [Focus ring composition](#focus-ring-composition))
|
|
693
|
+
The focus ring composition (see [Focus ring composition](#focus-ring-composition)) is a single `borderWidth.hairline` (1px) ring in `border.focused`.
|
|
685
694
|
|
|
686
695
|
**Sub-pixel widths are forbidden.** A 0.5px hairline renders inconsistently across DPR. For a thinner-than-1px effect, lower the stroke color's *opacity*, not the width.
|
|
687
696
|
|
|
@@ -703,21 +712,20 @@ Shadows are classified by **spatial role**, not by component. A card and a selec
|
|
|
703
712
|
|
|
704
713
|
Three lift levels plus one direction-special token. Each preset is a self-contained two-layer shadow (tight ambient + wider spread); components consume the role, never assemble shadows themselves.
|
|
705
714
|
|
|
706
|
-
- **Lift ramp** — `
|
|
715
|
+
- **Lift ramp** — `floating` (free-floating) → `overlay` (page-blocking). Each step deepens the spread layer; meaning is the spatial relationship (hovers-above vs blocks). Surfaces that merely sit on the page (cards at rest, list rows) stay flat and separate via border, not shadow.
|
|
707
716
|
- **Direction-special** — `sheet`. Same intensity as `floating`, offset inverted so the shadow projects *away from the anchored edge* (bottom sheets cast upward).
|
|
708
717
|
- **Two-layer composition.** Tight ambient layer + wider spread layer, mirroring physical light so edges stay crisp while the halo fades.
|
|
709
718
|
- **Shadow alphas come from the overlay palettes.** `palette.black.*` draws from the [base-unit ladder](#base-unit-ladder) read as percent — every shadow alpha (4 / 6 / 8 / 12 / 16 / 20%) is a ladder step.
|
|
710
719
|
|
|
711
720
|
| Token | Two-layer shadow | Role |
|
|
712
721
|
|----------------------|-----------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
|
|
713
|
-
| `elevation.raised` | `0 1px 2px black/4%, 0 2px 6px black/6%` | Subtle lift. Cards at rest, hovered list rows, selected menu items, buttons that should read as gently elevated without demanding attention. |
|
|
714
722
|
| `elevation.floating` | `0 2px 4px black/6%, 0 8px 20px black/12%` | Free-floating above the page. FABs, floating menus, dropdowns, autocomplete panels — elements that detach from the flow and hover over content. |
|
|
715
723
|
| `elevation.overlay` | `0 4px 12px black/8%, 0 16px 48px black/20%` | Page-level overlay demanding user focus. Modals, dialogs, popovers, full-screen prompts that sit above a scrim and block interaction below. |
|
|
716
724
|
| `elevation.sheet` | `0 -2px 6px black/4%, 0 -8px 24px black/16%` | Edge-anchored panel projecting shadow away from its anchored edge (here, anchored bottom — shadow rises). Bottom sheets, side drawers, pinned panels. |
|
|
717
725
|
|
|
718
726
|
### State layers & Focus
|
|
719
727
|
|
|
720
|
-
A single rule expresses every interactive state — paint a translucent layer of the element's foreground over its base, at the state's opacity — paired with a
|
|
728
|
+
A single rule expresses every interactive state — paint a translucent layer of the element's foreground over its base, at the state's opacity — paired with a single-ring focus indicator.
|
|
721
729
|
|
|
722
730
|
#### State overlays
|
|
723
731
|
|
|
@@ -732,10 +740,10 @@ Interactive controls need feedback for hover, focus, pressed, dragged across man
|
|
|
732
740
|
**How to apply**:
|
|
733
741
|
|
|
734
742
|
1. **Pick the overlay color** — it is the element's foreground.
|
|
735
|
-
- Filled primary button → `
|
|
736
|
-
- Tonal button on `
|
|
737
|
-
- Text / ghost button on `surface` → `
|
|
738
|
-
- Selectable surface (list row, menu item) → `
|
|
743
|
+
- Filled primary button → `text.onFill`.
|
|
744
|
+
- Tonal button on `background.selected` → `text.link`.
|
|
745
|
+
- Text / ghost button on `surface.default` → `text.link` (the ink becomes the overlay when there is no fill).
|
|
746
|
+
- Selectable surface (list row, menu item) → `text.default`.
|
|
739
747
|
2. **Pick the opacity from `state.*`** based on active state.
|
|
740
748
|
3. **Composite** — render the overlay as a layer (pseudo-element, extra background-image, or `color-mix`) clipped to the element's shape.
|
|
741
749
|
4. **Stack additively** when states coexist. Focused + hovered → 8% + 12% composited. Pressed during focus → 12% + 16%.
|
|
@@ -753,27 +761,26 @@ Interactive controls need feedback for hover, focus, pressed, dragged across man
|
|
|
753
761
|
|
|
754
762
|
#### Focus ring composition
|
|
755
763
|
|
|
756
|
-
The state overlay alone doesn't meet keyboard-accessibility contrast requirements. Every interactive control pairs `state.focus` with a visible ring on `:focus-visible` — a **
|
|
764
|
+
The state overlay alone doesn't meet keyboard-accessibility contrast requirements. Every interactive control pairs `state.focus` with a visible ring on `:focus-visible` — a **single 1px ring** in `border.focused`:
|
|
757
765
|
|
|
758
|
-
| Layer
|
|
759
|
-
|
|
760
|
-
| Fill layer
|
|
761
|
-
|
|
|
762
|
-
| Outer ring | 1px → 3px outside the control's edge | 2px | `color.focus` |
|
|
766
|
+
| Layer | Position relative to control | Width | Token |
|
|
767
|
+
|------------|---------------------------------|-------|--------------------------|
|
|
768
|
+
| Fill layer | painted ON the control's surface | — | `state.focus` (12%) of the variant's foreground composited over the variant's container |
|
|
769
|
+
| Ring | at the control's edge | 1px | `sys.color.border.focused` |
|
|
763
770
|
|
|
764
|
-
|
|
771
|
+
`border.focused` is **neutral** (near-black in light, mid-grey in dark), so the ring reads against any surface in the stack without competing with `border.primary`'s blue. It is a single ring — no coloured stroke and no white inset counter-ring. (The earlier two-layer outer-ring + inset-hairline composition is retired.)
|
|
765
772
|
|
|
766
773
|
The ring sits on a **dedicated overlay layer** — a `position: absolute` pseudo-element (`::after`) — not an `outline` / `box-shadow` on the control. The pseudo draws *on top of* the state-overlay tint and label, and **never affects layout** — focus moving across a row never reflows a sibling.
|
|
767
774
|
|
|
768
775
|
Two named compositions cover every control:
|
|
769
776
|
|
|
770
|
-
**Outward** — the default. Ring sits *outside* the control's footprint
|
|
777
|
+
**Outward** — the default. Ring sits *outside* the control's footprint. For controls that live inline with breathing room — **action affordances**: Button (every appearance), Chip, Form Field, FAB, Icon Button, Text Button. The outward extent is reserved by surrounding layout.
|
|
771
778
|
|
|
772
|
-
**Inward** — for **container-shaped components** filling their parent edge-to-edge: Tab Bar (slots flush at `flex: 1 1 0`), Tabs Underline (row in `overflow-x: auto` scroller), List (rows tile the column with a hairline divider). Drawn *inside* the bounding box
|
|
779
|
+
**Inward** — for **container-shaped components** filling their parent edge-to-edge: Tab Bar (slots flush at `flex: 1 1 0`), Tabs Underline (row in `overflow-x: auto` scroller), List (rows tile the column with a hairline divider). Drawn *inside* the bounding box, avoiding clipping at scrollers / overlap with neighbours / past dividers. Composition is identical to outward; only the offset flips. The ring inherits the control's `border-radius`. Suppressed while `disabled`.
|
|
773
780
|
|
|
774
781
|
**Choosing.** Default to **Outward**. Switch to **Inward** when *any* of: (a) the control is flush against a sibling, (b) the parent is an `overflow: hidden` / `overflow-x: auto` scroller, (c) the footprint tiles the available width (`flex: 1 1 0` slot, `width: 100%` list row). The choice is fixed per sub-component — never per-instance.
|
|
775
782
|
|
|
776
|
-
Canonical CSS recipe — write on a pseudo-element layer
|
|
783
|
+
Canonical CSS recipe — write on a pseudo-element layer:
|
|
777
784
|
|
|
778
785
|
```css
|
|
779
786
|
.control { position: relative; isolation: isolate; }
|
|
@@ -791,16 +798,12 @@ Canonical CSS recipe — write on a pseudo-element layer, write the multi-shadow
|
|
|
791
798
|
|
|
792
799
|
.control:focus-visible::after {
|
|
793
800
|
/* outward — the default */
|
|
794
|
-
box-shadow:
|
|
795
|
-
0 0 0 1px var(--sys-color-focusInset), /* inner counter-ring, on top */
|
|
796
|
-
0 0 0 3px var(--sys-color-focus); /* outer ring, visible at 1..3px */
|
|
801
|
+
box-shadow: 0 0 0 var(--sys-borderWidth-hairline) var(--sys-color-border-focused);
|
|
797
802
|
}
|
|
798
803
|
|
|
799
|
-
/* inside a scroller, re-anchor inward
|
|
804
|
+
/* inside a scroller, re-anchor inward (same single ring): */
|
|
800
805
|
.scroller .control:focus-visible::after {
|
|
801
|
-
box-shadow:
|
|
802
|
-
inset 0 0 0 2px var(--sys-color-focus), /* outer stroke at 0..2px in */
|
|
803
|
-
inset 0 0 0 3px var(--sys-color-focusInset); /* counter-ring visible at 2..3px in */
|
|
806
|
+
box-shadow: inset 0 0 0 var(--sys-borderWidth-hairline) var(--sys-color-border-focused);
|
|
804
807
|
}
|
|
805
808
|
|
|
806
809
|
.control:disabled::after { box-shadow: none; }
|
|
@@ -816,19 +819,19 @@ Text links diverge from [State overlays](#state-overlays) because they have no c
|
|
|
816
819
|
|
|
817
820
|
| State | Treatment |
|
|
818
821
|
|--------------|--------------------------------------------------------------------------------------------------|
|
|
819
|
-
| `default` | No decoration. Color is the link's resting ink (`
|
|
822
|
+
| `default` | No decoration. Color is the link's resting ink (`text.link` for accent links, `text.default` for navigation labels — whatever the surrounding type role specifies). |
|
|
820
823
|
| `hovered` | `text-decoration: underline`, `text-decoration-thickness: 1px`, `text-underline-offset: 2px`. **Color does not change** — the underline is the affordance. |
|
|
821
824
|
| `pressed` | Underline persists; opacity drops to `state.pressed` (16%) overlay on the text via `color-mix` so the link feels tactile without flipping its ink color. |
|
|
822
825
|
| `disabled` | Element opacity at `state.disabled` (40%); underline suppressed. |
|
|
823
|
-
| `focused` | Underline persists; the
|
|
826
|
+
| `focused` | Underline persists; the focus ring (see [Focus ring composition](#focus-ring-composition)) paints around the link's text box. |
|
|
824
827
|
|
|
825
828
|
Underline appears **only on hover** because resting text already inherits hierarchy from `typo.*` — a permanent underline would over-emphasize navigation chrome.
|
|
826
829
|
|
|
827
|
-
**Do not change color on hover.** A blue-on-hover (flipping to `
|
|
830
|
+
**Do not change color on hover.** A blue-on-hover (flipping to `text.link`) competes with the surrounding type's color hierarchy and reads as a category change. Underline owns hover; color owns role.
|
|
828
831
|
|
|
829
832
|
```css
|
|
830
833
|
.text-link {
|
|
831
|
-
color: inherit; /* or
|
|
834
|
+
color: inherit; /* or text.link for accent links */
|
|
832
835
|
text-decoration: none;
|
|
833
836
|
}
|
|
834
837
|
.text-link:hover {
|
|
@@ -837,7 +840,7 @@ Underline appears **only on hover** because resting text already inherits hierar
|
|
|
837
840
|
text-underline-offset: 2px;
|
|
838
841
|
}
|
|
839
842
|
.text-link:focus-visible {
|
|
840
|
-
/*
|
|
843
|
+
/* single-ring focus per Focus ring composition above */
|
|
841
844
|
}
|
|
842
845
|
```
|
|
843
846
|
|
|
@@ -849,20 +852,20 @@ The blinking insertion mark inside a text-input slot (`<input>`, `<textarea>`, c
|
|
|
849
852
|
|
|
850
853
|
| Property | Value | Token |
|
|
851
854
|
|------------------|------------------------------------------------|--------------------------------------|
|
|
852
|
-
| Color | High-emphasis foreground of the surface — same ink the user is typing | `sys.color.
|
|
855
|
+
| Color | High-emphasis foreground of the surface — same ink the user is typing | `sys.color.text.default` (default), `sys.color.text.danger` (error appearance) |
|
|
853
856
|
| Intended width | 2px | `sys.borderWidth.thin` |
|
|
854
857
|
| Intended height | 0.75 × the input's text line-box (computed from `line-height`) | derived |
|
|
855
858
|
| Intended ends | Rounded | radius equal to half the caret width |
|
|
856
859
|
|
|
857
860
|
**Color is the only part of the recipe browsers honour.** Standard CSS exposes `caret-color` and nothing else — width, height, end-cap shape are painted by the browser's text engine and can't be overridden without forfeiting the native input (and with it: IME composition, RTL, screen-reader cursor, mobile autocorrect). The width / height / rounded-ends columns are **design intent** the system asks browsers to approximate; the enforceable contract is colour.
|
|
858
861
|
|
|
859
|
-
**Why no Caret component.** A caret isn't compositional — it lives inside a text-input element, has no React tree, can't accept props. Wrapping it would either re-implement the input surface in JS over a `caret-color: transparent` field (breaks platform IME / a11y) or invent a token group with nothing to wire into. Every input-bearing component sets `caret-color: var(--sys-color-
|
|
862
|
+
**Why no Caret component.** A caret isn't compositional — it lives inside a text-input element, has no React tree, can't accept props. Wrapping it would either re-implement the input surface in JS over a `caret-color: transparent` field (breaks platform IME / a11y) or invent a token group with nothing to wire into. Every input-bearing component sets `caret-color: var(--sys-color-text-default)` (or `var(--sys-color-text-danger)` on error) and references this section.
|
|
860
863
|
|
|
861
864
|
```css
|
|
862
865
|
.chorus-input,
|
|
863
866
|
.chorus-field__input,
|
|
864
867
|
.chorus-navigation-bar__search-input {
|
|
865
|
-
caret-color: var(--sys-color-
|
|
868
|
+
caret-color: var(--sys-color-text-default);
|
|
866
869
|
}
|
|
867
870
|
|
|
868
871
|
.chorus-input.is-error,
|
|
@@ -871,7 +874,7 @@ The blinking insertion mark inside a text-input slot (`<input>`, `<textarea>`, c
|
|
|
871
874
|
}
|
|
872
875
|
```
|
|
873
876
|
|
|
874
|
-
**Inheritance shortcut** — when the input element's `color` is bound to the right ink (Form Field Input sets `color: var(--field-text)` resolving to `
|
|
877
|
+
**Inheritance shortcut** — when the input element's `color` is bound to the right ink (Form Field Input sets `color: var(--field-text)` resolving to `text.default` / `text.danger`), `caret-color` inherits automatically. Set `caret-color` explicitly only when the input's own `color` differs from the caret's intended colour.
|
|
875
878
|
|
|
876
879
|
### Responsive behavior
|
|
877
880
|
|
|
@@ -899,7 +902,7 @@ Per-group rules for the mobile→tablet (800px) step-up. **`md` is the responsiv
|
|
|
899
902
|
| `typo.display.lg` | +2 scale steps (48 → 80px) |
|
|
900
903
|
| `typo.heading.lg` | +1 scale step (24 → 32px) |
|
|
901
904
|
| `typo.display.md/sm`, `typo.heading.md/sm` | unchanged — already small enough to read on any viewport |
|
|
902
|
-
| `typo.body.*`, `typo.label
|
|
905
|
+
| `typo.body.*`, `typo.label.*` (incl. `label.xs`) | unchanged — reading and tap targets stay constant |
|
|
903
906
|
| `layout.*` at `lg` and above | +1 step (e.g. `layout.container.lg` 24 → 32px) |
|
|
904
907
|
| `layout.*` at `md` and below | unchanged — flat across the mobile↔web line |
|
|
905
908
|
| Elevation, radius, color, state | unchanged |
|
|
@@ -938,15 +941,15 @@ Accessibility is a property of the token system, not a checklist applied at the
|
|
|
938
941
|
|
|
939
942
|
#### Conformance targets
|
|
940
943
|
|
|
941
|
-
Chorus targets **WCAG 2.2 Level AA** as the floor for every product surface and **AAA where the foundations already meet it** (e.g. `
|
|
944
|
+
Chorus targets **WCAG 2.2 Level AA** as the floor for every product surface and **AAA where the foundations already meet it** (e.g. `text.default`/`surface.default` clears AAA at 7:1 in both modes). A surface that fails AA is a bug — fix or document the exception.
|
|
942
945
|
|
|
943
946
|
#### Color contrast
|
|
944
947
|
|
|
945
948
|
Enforced by the **paired-token rule**: every fill ships with its `on*` foreground, tuned to clear 4.5:1 for body text and 3:1 for large text and non-text UI.
|
|
946
949
|
|
|
947
|
-
- **Never read contrast manually.** If two roles aren't paired, they aren't a permitted combination. `
|
|
948
|
-
- **Surface
|
|
949
|
-
- **Lower-emphasis text uses `
|
|
950
|
+
- **Never read contrast manually.** If two roles aren't paired, they aren't a permitted combination. `text.default` on a `background.primary` fill bypasses the contract.
|
|
951
|
+
- **Surface is single-pair.** Both `surface.default` and `surface.sunken` read against `text.default`. Surface names carry *spatial meaning*, not contrast variation.
|
|
952
|
+
- **Lower-emphasis text uses `text.subtle`** — still ≥ 4.5:1 against every surface tone, one step lighter than `text.default` for two-tier hierarchy.
|
|
950
953
|
- **Disabled is the exception.** `state.disabled` (40% opacity) drops below AA on purpose — WCAG 1.4.3 inactive-component carve-out applies. Never use `disabled` styling to convey live information.
|
|
951
954
|
|
|
952
955
|
#### Touch & pointer targets
|
|
@@ -988,7 +991,7 @@ Every interactive control must be reachable, operable, and visible to a keyboard
|
|
|
988
991
|
- **Don't convey meaning by color alone.** Required markers, error states, status pills pair color with text or icon.
|
|
989
992
|
- **Resize support to 200%.** Type scales in rem; layout doesn't break, no horizontal scroll at zoom 200%.
|
|
990
993
|
- **Reflow at 320 CSS pixels.** Mobile-narrow content reflows without horizontal scroll except for elements needing 2D scrolling (tables, code blocks, maps).
|
|
991
|
-
- **`prefers-contrast: more`** — increase border weight from `borderWidth.hairline` to `borderWidth.thin`, switch `
|
|
994
|
+
- **`prefers-contrast: more`** — increase border weight from `borderWidth.hairline` to `borderWidth.thin`, switch `border.default` to `border.bold`, drop tonal elevation in favor of explicit borders.
|
|
992
995
|
- **Plain language.** Use [Voice & Content](#voice--content) rules.
|
|
993
996
|
|
|
994
997
|
#### Internationalization
|
|
@@ -1016,12 +1019,12 @@ Quick rules, paired 1-to-1: each Do has a matching Don't.
|
|
|
1016
1019
|
- **Pair every accent fill with its `on*` foreground.** Pairs are tuned to clear AA.
|
|
1017
1020
|
- **Reach for `XContainer` + `onXContainer` for tinted surfaces.** Callouts, info banners, success tiles, selected rows — the Container tone is the tint.
|
|
1018
1021
|
- **Compose state as foreground-over-base.** `state.*` opacity over the element's foreground — works on every variant.
|
|
1019
|
-
- **Express lift with `elevation.*` shadows.**
|
|
1022
|
+
- **Express lift with `elevation.*` shadows.** Surface names carry spatial *meaning* even when the tone is identical.
|
|
1020
1023
|
- **Use `layout.*` for layout-participating spacing.** Page gutters, card insets, section rhythm grow on web; raw `space.*` only for fixed-footprint controls.
|
|
1021
1024
|
- **Apply `layout.page.*` once at the route root.** Nested content uses `layout.container.*` / `layout.stack.*` / `layout.inline.*`.
|
|
1022
1025
|
- **Use Pretendard for both Hangul and Latin.**
|
|
1023
1026
|
- **Use `radius.md` for controls and `radius.xl` for surfaces.** Containers visually "hold" the controls inside them.
|
|
1024
|
-
- **Build every `:focus-visible` ring from the
|
|
1027
|
+
- **Build every `:focus-visible` ring from the standard composition.** A single 1px `border.focused` ring plus the `state.focus` fill overlay.
|
|
1025
1028
|
|
|
1026
1029
|
#### Don't
|
|
1027
1030
|
|
|
@@ -1030,12 +1033,12 @@ Quick rules, paired 1-to-1: each Do has a matching Don't.
|
|
|
1030
1033
|
- **Don't read foreground contrast manually or mix `on*` across roles.** A handpicked text color silently breaks AA as the palette evolves.
|
|
1031
1034
|
- **Don't compose ad-hoc tinted surfaces with `color-mix(<accent> N%, <surface>)`.** A 5–10% accent over `surface*` for a callout, banner, info block, or "subtle" highlight bypasses the Container quartet's AA contract. The only allowed `color-mix` involving an accent is the [state-overlay formula](#state-overlays) or a [decorative gradient stop fading to `transparent`](#four-token-quartet) where text contrast is governed by the underlying base.
|
|
1032
1035
|
- **Don't hardcode hover or pressed colors per component.**
|
|
1033
|
-
- **Don't add tonal elevation in light mode.**
|
|
1036
|
+
- **Don't add tonal elevation in light mode.** `surface.default` resolves to `#fcfcfd` and lift comes from `elevation.*` shadows, not brighter fills.
|
|
1034
1037
|
- **Don't reach for raw `space.*` for layout-level rhythm.** Section gaps, card-stack rhythm, page gutters live in `layout.*`.
|
|
1035
1038
|
- **Don't reapply `layout.page.*` to nested content.** Full-bleed elements opt out by negating the gutter, not by changing the token.
|
|
1036
1039
|
- **Don't substitute Latin-only or Korean-only fonts per region.**
|
|
1037
1040
|
- **Don't introduce per-corner radius tokens.** They multiply the token surface and don't survive a global radius change.
|
|
1038
|
-
- **Don't
|
|
1041
|
+
- **Don't recolour the focus ring per control.** `border.focused` is neutral by design so it reads on any surface; a per-control hue (e.g. blue on a blue button) can vanish.
|
|
1039
1042
|
|
|
1040
1043
|
---
|
|
1041
1044
|
|
|
@@ -1080,7 +1083,7 @@ Three lines max: **what this surface is for · why it's empty · the one action
|
|
|
1080
1083
|
|
|
1081
1084
|
- ✅ "No posts yet. Conversations you start or join will appear here. **Start a post.**"
|
|
1082
1085
|
|
|
1083
|
-
The CTA is often the surface's primary action — make it primary visually too (`
|
|
1086
|
+
The CTA is often the surface's primary action — make it primary visually too (`background.primary` button).
|
|
1084
1087
|
|
|
1085
1088
|
#### Loading & success
|
|
1086
1089
|
|
|
@@ -1148,16 +1151,16 @@ The trap is splitting on *how different it looks*. `outlined` looks nothing like
|
|
|
1148
1151
|
|
|
1149
1152
|
Three lines max — see [Empty states](#empty-states) in Voice & Content for writing rules. Visual composition:
|
|
1150
1153
|
|
|
1151
|
-
- **Optional illustration** at `icon.xl` or larger, centered, color `
|
|
1152
|
-
- **Headline** in `typo.heading.sm` color `
|
|
1153
|
-
- **Body** in `typo.body.sm` color `
|
|
1154
|
+
- **Optional illustration** at `icon.xl` or larger, centered, color `text.subtle` (illustrations stay monochrome unless they carry brand-moment intent).
|
|
1155
|
+
- **Headline** in `typo.heading.sm` color `text.default`, `layout.stack.sm` below illustration.
|
|
1156
|
+
- **Body** in `typo.body.sm` color `text.subtle`, `layout.stack.2xs` below headline.
|
|
1154
1157
|
- **Primary CTA** as a default-size primary button, `layout.stack.md` below body.
|
|
1155
1158
|
- Whole composition centered inside the surface that would otherwise hold the data.
|
|
1156
1159
|
|
|
1157
1160
|
### Loading & Skeleton states
|
|
1158
1161
|
|
|
1159
|
-
- **Spinners** for indeterminate loads under ~1 second of expected wait. Use `
|
|
1160
|
-
- **Skeleton placeholders** for content shapes that will arrive — feed cards, list rows, profile headers. Skeleton color is `
|
|
1162
|
+
- **Spinners** for indeterminate loads under ~1 second of expected wait. Use `background.primary` for foreground motion on neutral surfaces; reserve to a single spinner per view.
|
|
1163
|
+
- **Skeleton placeholders** for content shapes that will arrive — feed cards, list rows, profile headers. Skeleton color is `background.neutral`; the pulse animation runs at 1.5–2 Hz (well below the WCAG flash threshold) and respects `prefers-reduced-motion: reduce` (no animation; show the skeleton statically).
|
|
1161
1164
|
- **Match the shape**, not the content. A skeleton for a feed card uses the same radius, padding, and inline rhythm as the real card so the layout doesn't jump on resolution.
|
|
1162
1165
|
- **Don't skeleton tiny surfaces.** A spinner is faster than authoring a skeleton for a 40px button.
|
|
1163
1166
|
- **Loading copy** lives inside the skeleton or beside the spinner — see [Loading & success](#loading--success) for the writing rule.
|
|
@@ -1246,13 +1249,13 @@ Chorus-specific vocabulary. The section introducing each term is canonical.
|
|
|
1246
1249
|
- **System tier (`sys.*`)** — Semantic roles that consume the reference tier and form the vocabulary product surfaces speak in. The default tier for any product code.
|
|
1247
1250
|
- **Component tier (`comp.*`)** — Per-component tokens that bind system roles to a component's contract. Opt-in; currently empty by design.
|
|
1248
1251
|
- **Quartet** — The fixed four-token unit every accent role ships as: `X` / `onX` / `XContainer` / `onXContainer`. The unit of meaning; never use a fill without its `on*`. See [Four-token quartet](#four-token-quartet).
|
|
1249
|
-
- **
|
|
1252
|
+
- **Surface roles** — `surface.default` (page / card ground) and `surface.sunken` (gutters, wells). Two planes, not a tonal ladder; spatial meaning without ever-brightening fills.
|
|
1250
1253
|
- **Base-unit ladder** — The single canonical numeric ladder (`0 · 2 · 4 · 6 · 8 · 12 · 16 · 20 · 24 · 40 · 48 · 64 · 80`) that spacing (px), type (px), and opacity (%) all draw from. See [Base-unit ladder](#base-unit-ladder).
|
|
1251
1254
|
- **Layout axis** — One of four orthogonal spacing roles: `page`, `container`, `stack`, `inline`. Each owns one spatial relationship and is applied by exactly one kind of element.
|
|
1252
1255
|
- **Veil / Scrim / Endpoint** — The three opacity bands of `palette.black` / `palette.white`: veil (4–24%) for state overlays, scrim (40–80%) for backdrop dimming, endpoint (0% / 100%) for reset and fully-opaque uses.
|
|
1253
|
-
- **Tonal elevation** — the pattern of expressing lift via brighter surface tones at higher elevation. Chorus *
|
|
1256
|
+
- **Tonal elevation** — the pattern of expressing lift via brighter surface tones at higher elevation. Chorus *does not* use tonal elevation; lift comes from `elevation.*` shadows and surface tone stays flat.
|
|
1254
1257
|
- **State overlay** — The single rule that paints a translucent layer of an element's *foreground color* over its *base*, at the opacity defined by `state.*`. One rule, every variant. See [State overlays](#state-overlays).
|
|
1255
|
-
- **Focus ring composition** — The
|
|
1258
|
+
- **Focus ring composition** — The focus indicator: a single 1px `border.focused` ring plus a `state.focus` fill overlay. Every interactive control uses the same composition.
|
|
1256
1259
|
- **Slot** — A named region inside a component anatomy (`container`, `label`, `leadingIcon`, …). Tokens bind to slots, not to components as a whole.
|
|
1257
1260
|
- **Sub-component** ("type") — A distinct anchoring role within a family that forks slot vocabulary or sizing (Icon vs Standard button), shipped as its own `<sub>.spec.json`. Forced by a *contract* change — a required slot, interaction model, or anchoring role differs — never by visual distance alone. See [Sub-component or case](#sub-component-or-case-the-contract-test).
|
|
1258
1261
|
- **Composition case** — A configuration of one sub-component where *optional* slots are present or absent (`With leading icon`). The contract is unchanged; demonstrated under `## Use cases`, never forked into a sub-component.
|
|
@@ -1269,36 +1272,34 @@ Mapping from common UI needs to system tokens.
|
|
|
1269
1272
|
|
|
1270
1273
|
| Need | Token | Light value |
|
|
1271
1274
|
|-----------------------|--------------------------------------------------|---------------------|
|
|
1272
|
-
| Page background | `
|
|
1273
|
-
| Primary text | `color.
|
|
1274
|
-
| Secondary text | `color.
|
|
1275
|
-
| Card surface | `
|
|
1276
|
-
| Card border | `color.
|
|
1277
|
-
| Primary CTA fill | `color.primary`
|
|
1278
|
-
| Primary CTA text | `color.
|
|
1279
|
-
| Link | `color.
|
|
1280
|
-
| Error | `color.
|
|
1281
|
-
| Success
|
|
1282
|
-
| Focus ring
|
|
1283
|
-
| Focus ring (inner) | `color.focusInset` | `#ffffff` |
|
|
1275
|
+
| Page background | `surface.default` | `#fcfcfd` |
|
|
1276
|
+
| Primary text | `color.text.default` | `#1e2024` |
|
|
1277
|
+
| Secondary text | `color.text.subtle` | `#5e6573` |
|
|
1278
|
+
| Card surface | `surface.default` | `#fcfcfd` |
|
|
1279
|
+
| Card border | `color.border.default` | `#eef0f5` |
|
|
1280
|
+
| Primary CTA fill | `color.background.primary` | `#007aff` |
|
|
1281
|
+
| Primary CTA text | `color.text.onFill` | `#fcfcfd` |
|
|
1282
|
+
| Link | `color.text.link` | `#007aff` |
|
|
1283
|
+
| Error | `color.text.danger` | `#e83a3a` |
|
|
1284
|
+
| Success (+ ✓ icon) | `color.text.success` | `#007aff` |
|
|
1285
|
+
| Focus ring | `color.border.focused` | `#007aff` |
|
|
1284
1286
|
| Card padding | `layout.container.md` | 16 → 24px |
|
|
1285
1287
|
| Page gutter | `layout.page.md` | 16 → 32px |
|
|
1286
1288
|
| Section rhythm | `layout.stack.lg` | 24 → 32px |
|
|
1287
1289
|
| Control radius | `radius.md` | 8px |
|
|
1288
1290
|
| Surface radius | `radius.xl` | 16px |
|
|
1289
|
-
| Card shadow | `elevation.raised` | two-layer ambient + spread |
|
|
1290
1291
|
|
|
1291
1292
|
#### Example component prompts
|
|
1292
1293
|
|
|
1293
1294
|
Reference prompts resolving through the system tier — copy and adapt.
|
|
1294
1295
|
|
|
1295
|
-
> "Build a primary button: `
|
|
1296
|
+
> "Build a primary button: `background.primary` background, `text.onFill` text, `radius.md` corners, `layout.container.sm` vertical padding and `layout.container.md` horizontal padding, `typo.label.md` for the label. On `:hover`, composite an 8% `text.onFill` overlay; on `:focus-visible`, apply the single-ring focus indicator (see [Focus ring composition](#focus-ring-composition))."
|
|
1296
1297
|
|
|
1297
|
-
> "Design a content card: `
|
|
1298
|
+
> "Design a content card: `surface.default` background, `radius.xl` corners, `1px solid border.default` border (cards stay flat — no shadow), `layout.container.md` padding. Title in `typo.heading.md` `text.default`; body in `typo.body.md` `text.subtle`. Stack title and body with `layout.stack.sm`."
|
|
1298
1299
|
|
|
1299
|
-
> "Create a form field: `
|
|
1300
|
+
> "Create a form field: `surface.sunken` background, `radius.md` corners, `layout.container.sm` padding. Label above in `typo.label.sm` `text.subtle`. Border `1px solid border.default`; on focus, border swaps to `border.focused` and the single-ring focus indicator paints. Error state: border swaps to `border.danger`, helper text uses `text.danger` at `typo.label.sm`."
|
|
1300
1301
|
|
|
1301
|
-
> "Build
|
|
1302
|
+
> "Build an information banner: `background.information` background, `text.link` text and icons, `radius.lg` corners, `layout.container.md` padding. Inline with `layout.inline.md` between icon and text. No shadow — banners stay flat."
|
|
1302
1303
|
|
|
1303
1304
|
#### Iteration rules
|
|
1304
1305
|
|