@discourser/design-system 0.3.0 → 0.3.1

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.
@@ -0,0 +1,457 @@
1
+ # Switch
2
+
3
+ **Purpose:** Toggle control for binary on/off states following Material Design 3 patterns.
4
+
5
+ ## Import
6
+
7
+ ```typescript
8
+ import { Switch } from '@discourser/design-system';
9
+ ```
10
+
11
+ ## Overview
12
+
13
+ The Switch component provides:
14
+ - Binary on/off toggle functionality
15
+ - Visual feedback for state changes
16
+ - Smooth animation between states
17
+ - Built-in label support
18
+ - Form integration capabilities
19
+ - Keyboard accessibility
20
+
21
+ ## Sizes
22
+
23
+ | Size | Track Width | Track Height | Thumb Size (off) | Thumb Size (on) | Label Size |
24
+ |------|------------|--------------|------------------|-----------------|-----------|
25
+ | `sm` | 44px | 24px | 12×12px | 16×16px | bodySmall |
26
+ | `md` | 52px | 32px | 16×16px | 24×24px | bodyMedium |
27
+
28
+ **Note:** The thumb (circle) grows larger when the switch is in the "on" state, following M3 specifications.
29
+
30
+ ## Props
31
+
32
+ | Prop | Type | Default | Description |
33
+ |------|------|---------|-------------|
34
+ | `label` | `string` | - | Label text displayed next to switch |
35
+ | `checked` | `boolean` | - | Controlled checked state |
36
+ | `defaultChecked` | `boolean` | - | Uncontrolled default checked state |
37
+ | `onCheckedChange` | `(details: { checked: boolean }) => void` | - | Callback when checked state changes |
38
+ | `disabled` | `boolean` | `false` | Disable switch interaction |
39
+ | `name` | `string` | - | Name attribute for form submission |
40
+ | `value` | `string` | - | Value attribute for form submission |
41
+ | `required` | `boolean` | `false` | Whether switch is required in form |
42
+ | `size` | `'sm' \| 'md'` | `'md'` | Switch size |
43
+
44
+ ## Examples
45
+
46
+ ### Basic Usage
47
+
48
+ ```typescript
49
+ // Uncontrolled switch (default off)
50
+ <Switch label="Enable notifications" />
51
+
52
+ // Uncontrolled with default checked
53
+ <Switch label="Dark mode" defaultChecked />
54
+
55
+ // Controlled switch
56
+ const [enabled, setEnabled] = useState(false);
57
+
58
+ <Switch
59
+ label="Email notifications"
60
+ checked={enabled}
61
+ onCheckedChange={({ checked }) => setEnabled(checked)}
62
+ />
63
+ ```
64
+
65
+ ### Different Sizes
66
+
67
+ ```typescript
68
+ // Small switch
69
+ <Switch size="sm" label="Compact toggle" />
70
+
71
+ // Medium switch (default)
72
+ <Switch size="md" label="Standard toggle" />
73
+ ```
74
+
75
+ ### Disabled State
76
+
77
+ ```typescript
78
+ <Switch label="Disabled (off)" disabled />
79
+
80
+ <Switch label="Disabled (on)" disabled defaultChecked />
81
+ ```
82
+
83
+ ### Without Label
84
+
85
+ ```typescript
86
+ // Switch without label (ensure you provide accessibility context elsewhere)
87
+ <Switch />
88
+ ```
89
+
90
+ ## Common Patterns
91
+
92
+ ### Settings Panel
93
+
94
+ ```typescript
95
+ const [settings, setSettings] = useState({
96
+ notifications: true,
97
+ darkMode: false,
98
+ autoSave: true,
99
+ });
100
+
101
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'md' })}>
102
+ <Switch
103
+ label="Push notifications"
104
+ checked={settings.notifications}
105
+ onCheckedChange={({ checked }) =>
106
+ setSettings({ ...settings, notifications: checked })
107
+ }
108
+ />
109
+ <Switch
110
+ label="Dark mode"
111
+ checked={settings.darkMode}
112
+ onCheckedChange={({ checked }) =>
113
+ setSettings({ ...settings, darkMode: checked })
114
+ }
115
+ />
116
+ <Switch
117
+ label="Auto-save"
118
+ checked={settings.autoSave}
119
+ onCheckedChange={({ checked }) =>
120
+ setSettings({ ...settings, autoSave: checked })
121
+ }
122
+ />
123
+ </div>
124
+ ```
125
+
126
+ ### Form Integration
127
+
128
+ ```typescript
129
+ <form onSubmit={handleSubmit}>
130
+ <Switch
131
+ name="terms"
132
+ value="accepted"
133
+ label="I agree to the terms and conditions"
134
+ required
135
+ />
136
+
137
+ <Switch
138
+ name="newsletter"
139
+ value="subscribed"
140
+ label="Subscribe to newsletter (optional)"
141
+ />
142
+
143
+ <Button type="submit">Submit</Button>
144
+ </form>
145
+ ```
146
+
147
+ ### With Description
148
+
149
+ ```typescript
150
+ import { css } from '@discourser/design-system/styled-system/css';
151
+
152
+ <div className={css({ display: 'flex', alignItems: 'flex-start', gap: 'sm' })}>
153
+ <Switch
154
+ checked={autoBackup}
155
+ onCheckedChange={({ checked }) => setAutoBackup(checked)}
156
+ />
157
+ <div>
158
+ <label className={css({ textStyle: 'bodyMedium', fontWeight: 500 })}>
159
+ Automatic backups
160
+ </label>
161
+ <p className={css({ textStyle: 'bodySmall', color: 'onSurfaceVariant', mt: 'xxs' })}>
162
+ Your data will be backed up automatically every 24 hours
163
+ </p>
164
+ </div>
165
+ </div>
166
+ ```
167
+
168
+ ### Dynamic Enable/Disable
169
+
170
+ ```typescript
171
+ const [featureEnabled, setFeatureEnabled] = useState(false);
172
+ const [advancedMode, setAdvancedMode] = useState(false);
173
+
174
+ <>
175
+ <Switch
176
+ label="Enable advanced features"
177
+ checked={featureEnabled}
178
+ onCheckedChange={({ checked }) => {
179
+ setFeatureEnabled(checked);
180
+ if (!checked) setAdvancedMode(false); // Reset dependent switch
181
+ }}
182
+ />
183
+
184
+ <Switch
185
+ label="Advanced mode"
186
+ checked={advancedMode}
187
+ onCheckedChange={({ checked }) => setAdvancedMode(checked)}
188
+ disabled={!featureEnabled} // Only enabled when feature is on
189
+ />
190
+ </>
191
+ ```
192
+
193
+ ### List of Toggleable Items
194
+
195
+ ```typescript
196
+ const [items, setItems] = useState([
197
+ { id: 1, name: 'Feature A', enabled: true },
198
+ { id: 2, name: 'Feature B', enabled: false },
199
+ { id: 3, name: 'Feature C', enabled: true },
200
+ ]);
201
+
202
+ const toggleItem = (id: number) => {
203
+ setItems(items.map(item =>
204
+ item.id === id ? { ...item, enabled: !item.enabled } : item
205
+ ));
206
+ };
207
+
208
+ <div className={css({ display: 'flex', flexDirection: 'column', gap: 'sm' })}>
209
+ {items.map(item => (
210
+ <Switch
211
+ key={item.id}
212
+ label={item.name}
213
+ checked={item.enabled}
214
+ onCheckedChange={() => toggleItem(item.id)}
215
+ />
216
+ ))}
217
+ </div>
218
+ ```
219
+
220
+ ## DO NOT
221
+
222
+ ```typescript
223
+ // ❌ Don't use checkbox for on/off toggles (use Switch instead)
224
+ <input type="checkbox" /> Enable feature
225
+
226
+ // ✅ Use Switch for binary toggles
227
+ <Switch label="Enable feature" />
228
+
229
+ // ❌ Don't use Switch for multiple choice (use radio or checkbox)
230
+ <Switch label="Option A" />
231
+ <Switch label="Option B" />
232
+ <Switch label="Option C" /> // Wrong for mutually exclusive options
233
+
234
+ // ✅ Use radio buttons for mutually exclusive choices
235
+ <input type="radio" name="option" value="a" /> Option A
236
+ <input type="radio" name="option" value="b" /> Option B
237
+
238
+ // ❌ Don't use Switch for submit actions (use Button instead)
239
+ <Switch label="Submit form" /> // Wrong
240
+
241
+ // ✅ Use Button for actions
242
+ <Button type="submit">Submit</Button>
243
+
244
+ // ❌ Don't nest switches or use as navigation
245
+ <Switch label="Navigate to settings" onClick={() => navigate('/settings')} /> // Wrong
246
+ ```
247
+
248
+ ## Accessibility
249
+
250
+ The Switch component follows WCAG 2.1 Level AA standards:
251
+
252
+ - **Keyboard Navigation**: Toggle with Space key, focus with Tab
253
+ - **ARIA Attributes**: Uses role="switch", aria-checked for state
254
+ - **Labels**: Label associated with switch for screen readers
255
+ - **Focus Indicator**: Visible focus state
256
+ - **State Announcement**: State changes announced to screen readers
257
+ - **Touch Target**: Adequate size for touch interaction
258
+
259
+ ### Accessibility Best Practices
260
+
261
+ ```typescript
262
+ // ✅ Always provide labels for clarity
263
+ <Switch label="Enable notifications" />
264
+
265
+ // ✅ Use descriptive labels
266
+ <Switch label="Receive email updates" /> // Clear what it toggles
267
+
268
+ // ❌ Vague labels
269
+ <Switch label="Enable" /> // Enable what?
270
+
271
+ // ✅ Indicate state in label if needed
272
+ <Switch
273
+ label={darkMode ? 'Dark mode (on)' : 'Dark mode (off)'}
274
+ checked={darkMode}
275
+ onCheckedChange={({ checked }) => setDarkMode(checked)}
276
+ />
277
+
278
+ // ✅ Provide context for switch without visible label
279
+ <Switch
280
+ aria-label="Enable push notifications for this conversation"
281
+ />
282
+ ```
283
+
284
+ ## State Behaviors
285
+
286
+ | State | Visual Change | Behavior |
287
+ |-------|---------------|----------|
288
+ | **Unchecked** | Track: `surfaceContainerHighest` with `outline` border<br />Thumb: Small, `outline` color, left position | Off state |
289
+ | **Checked** | Track: `primary` color<br />Thumb: Larger, `onPrimary` color, right position | On state |
290
+ | **Hover** | Subtle visual feedback | Interactive feedback |
291
+ | **Focus** | Focus indicator (handled by Ark UI) | Keyboard accessibility |
292
+ | **Disabled** | 38% opacity, greyed out | Cannot be toggled |
293
+ | **Animation** | Smooth thumb transition (fast easing) | Visual confirmation of state change |
294
+
295
+ ## Visual States
296
+
297
+ ### Unchecked (Off)
298
+ - Track background: `surfaceContainerHighest`
299
+ - Track border: 2px `outline`
300
+ - Thumb: Small (16×16px for md), `outline` color
301
+ - Thumb position: Left
302
+
303
+ ### Checked (On)
304
+ - Track background: `primary`
305
+ - Track border: 2px `primary`
306
+ - Thumb: Large (24×24px for md), `onPrimary` color
307
+ - Thumb position: Right
308
+
309
+ ### Disabled
310
+ - Track background: `surfaceVariant`
311
+ - Track border: `onSurface` (12% opacity)
312
+ - Thumb: `onSurface` (38% opacity)
313
+ - Overall opacity: 38%
314
+
315
+ ## Form Integration
316
+
317
+ ```typescript
318
+ // React Hook Form
319
+ import { useForm, Controller } from 'react-hook-form';
320
+
321
+ const { control, handleSubmit } = useForm();
322
+
323
+ <form onSubmit={handleSubmit(onSubmit)}>
324
+ <Controller
325
+ name="notifications"
326
+ control={control}
327
+ defaultValue={false}
328
+ render={({ field }) => (
329
+ <Switch
330
+ label="Enable notifications"
331
+ checked={field.value}
332
+ onCheckedChange={({ checked }) => field.onChange(checked)}
333
+ />
334
+ )}
335
+ />
336
+ </form>
337
+
338
+ // Formik
339
+ import { useFormik } from 'formik';
340
+
341
+ const formik = useFormik({
342
+ initialValues: { darkMode: false },
343
+ onSubmit: values => { /* ... */ },
344
+ });
345
+
346
+ <Switch
347
+ label="Dark mode"
348
+ name="darkMode"
349
+ checked={formik.values.darkMode}
350
+ onCheckedChange={({ checked }) => formik.setFieldValue('darkMode', checked)}
351
+ />
352
+ ```
353
+
354
+ ## Use Cases
355
+
356
+ | Use Case | Recommendation |
357
+ |----------|---------------|
358
+ | Enable/disable feature | ✅ Perfect use case |
359
+ | On/off settings | ✅ Perfect use case |
360
+ | Binary preferences | ✅ Perfect use case |
361
+ | Show/hide sections | ✅ Good use case |
362
+ | Mutually exclusive options | ❌ Use radio buttons |
363
+ | Multiple selections | ❌ Use checkboxes |
364
+ | Trigger actions | ❌ Use buttons |
365
+
366
+ ## Responsive Considerations
367
+
368
+ ```typescript
369
+ // Mobile: Consider using md (default) for better touch targets
370
+ <Switch size="md" label="Enable feature" />
371
+
372
+ // Desktop: Can use sm for denser layouts
373
+ <Switch size={{ base: 'md', lg: 'sm' }} label="Enable feature" />
374
+
375
+ // Settings panel with responsive spacing
376
+ <div className={css({
377
+ display: 'flex',
378
+ flexDirection: 'column',
379
+ gap: { base: 'md', lg: 'sm' }
380
+ })}>
381
+ <Switch label="Notification 1" />
382
+ <Switch label="Notification 2" />
383
+ <Switch label="Notification 3" />
384
+ </div>
385
+ ```
386
+
387
+ ## Testing
388
+
389
+ ```typescript
390
+ import { render, screen } from '@testing-library/react';
391
+ import userEvent from '@testing-library/user-event';
392
+
393
+ test('switch toggles checked state', async () => {
394
+ const handleChange = vi.fn();
395
+ render(
396
+ <Switch
397
+ label="Enable feature"
398
+ checked={false}
399
+ onCheckedChange={handleChange}
400
+ />
401
+ );
402
+
403
+ const switchControl = screen.getByRole('switch', { name: 'Enable feature' });
404
+ expect(switchControl).not.toBeChecked();
405
+
406
+ await userEvent.click(switchControl);
407
+
408
+ expect(handleChange).toHaveBeenCalledWith({ checked: true });
409
+ });
410
+
411
+ test('switch respects disabled state', async () => {
412
+ const handleChange = vi.fn();
413
+ render(
414
+ <Switch
415
+ label="Disabled switch"
416
+ disabled
417
+ onCheckedChange={handleChange}
418
+ />
419
+ );
420
+
421
+ const switchControl = screen.getByRole('switch', { name: 'Disabled switch' });
422
+ await userEvent.click(switchControl);
423
+
424
+ expect(handleChange).not.toHaveBeenCalled();
425
+ });
426
+
427
+ test('switch works with keyboard', async () => {
428
+ const handleChange = vi.fn();
429
+ render(
430
+ <Switch
431
+ label="Keyboard test"
432
+ checked={false}
433
+ onCheckedChange={handleChange}
434
+ />
435
+ );
436
+
437
+ const switchControl = screen.getByRole('switch', { name: 'Keyboard test' });
438
+ switchControl.focus();
439
+
440
+ await userEvent.keyboard(' '); // Space key
441
+
442
+ expect(handleChange).toHaveBeenCalledWith({ checked: true });
443
+ });
444
+ ```
445
+
446
+ ## When to Use Switch vs Checkbox
447
+
448
+ | Feature | Switch | Checkbox |
449
+ |---------|--------|----------|
450
+ | **Purpose** | Toggle state (on/off) | Select option(s) |
451
+ | **Effect** | Immediate | Usually requires submit |
452
+ | **State** | Active/inactive | Selected/unselected |
453
+ | **Typical Use** | Settings, preferences | Forms, multi-select |
454
+ | **Example** | "Enable dark mode" | "I agree to terms" |
455
+ | **Visual** | Track + thumb | Box + checkmark |
456
+
457
+ **Rule of thumb**: Use Switch when the change takes effect immediately. Use Checkbox when part of a form that needs submission.
@@ -0,0 +1,187 @@
1
+ # Color Tokens
2
+
3
+ The design system uses Material Design 3 semantic color tokens. **Always use semantic tokens, never raw hex values.**
4
+
5
+ ## Why Semantic Colors?
6
+
7
+ Semantic colors automatically adapt to light/dark themes and follow M3 color roles. Using semantic names ensures:
8
+ - Automatic theme switching
9
+ - Consistent contrast ratios
10
+ - Proper color relationships
11
+ - Accessibility compliance
12
+
13
+ ## Semantic Colors Reference
14
+
15
+ ### Primary Colors
16
+ Used for primary actions, key UI elements, and brand identity.
17
+
18
+ | Token | Light Mode | Dark Mode | Usage |
19
+ |-------|-----------|-----------|-------|
20
+ | `primary` | #4C662B | #B1D18A | Primary buttons, active states, links |
21
+ | `onPrimary` | #FFFFFF | #1F3701 | Text/icons on primary color |
22
+ | `primaryContainer` | #CDEDA3 | #354E16 | Containers for primary content |
23
+ | `onPrimaryContainer` | #354E16 | #CDEDA3 | Text/icons on primary container |
24
+
25
+ ### Secondary Colors
26
+ Used for secondary actions and less prominent UI elements.
27
+
28
+ | Token | Light Mode | Dark Mode | Usage |
29
+ |-------|-----------|-----------|-------|
30
+ | `secondary` | #586249 | #BFCBAD | Secondary buttons, less prominent actions |
31
+ | `onSecondary` | #FFFFFF | #2A331E | Text/icons on secondary color |
32
+ | `secondaryContainer` | #DCE7C8 | #404A33 | Secondary containers |
33
+ | `onSecondaryContainer` | #404A33 | #DCE7C8 | Text/icons on secondary container |
34
+
35
+ ### Tertiary Colors
36
+ Used for accent colors and tertiary actions.
37
+
38
+ | Token | Light Mode | Dark Mode | Usage |
39
+ |-------|-----------|-----------|-------|
40
+ | `tertiary` | #386663 | #A0D0CB | Accent colors, tertiary actions |
41
+ | `onTertiary` | #FFFFFF | #003735 | Text/icons on tertiary color |
42
+ | `tertiaryContainer` | #BCECE7 | #1F4E4B | Tertiary containers |
43
+ | `onTertiaryContainer` | #1F4E4B | #BCECE7 | Text/icons on tertiary container |
44
+
45
+ ### Error Colors
46
+ Used for error states, warnings, and destructive actions.
47
+
48
+ | Token | Light Mode | Dark Mode | Usage |
49
+ |-------|-----------|-----------|-------|
50
+ | `error` | #BA1A1A | #FFB4AB | Error text, error icons |
51
+ | `onError` | #FFFFFF | #690005 | Text/icons on error color |
52
+ | `errorContainer` | #FFDAD6 | #93000A | Error message backgrounds |
53
+ | `onErrorContainer` | #93000A | #FFDAD6 | Text/icons in error containers |
54
+
55
+ ### Surface Colors
56
+ Used for backgrounds and containers at different elevations.
57
+
58
+ | Token | Light Mode | Dark Mode | Usage |
59
+ |-------|-----------|-----------|-------|
60
+ | `surface` | #F9FAEF | #12140E | Default background |
61
+ | `onSurface` | #1A1C16 | #E2E3D8 | Default text color |
62
+ | `surfaceVariant` | #E1E4D5 | #44483D | Alternate surface (subtle contrast) |
63
+ | `onSurfaceVariant` | #44483D | #C5C8BA | Text on variant surfaces |
64
+
65
+ ### Surface Container Elevations
66
+ M3 uses surface tints instead of shadows for elevation. Higher elevations get lighter in light mode, darker in dark mode.
67
+
68
+ | Token | Light Mode | Dark Mode | Usage |
69
+ |-------|-----------|-----------|-------|
70
+ | `surfaceContainerLowest` | #FFFFFF | #0C0F09 | Lowest elevation (0dp) |
71
+ | `surfaceContainerLow` | #F3F4E9 | #1A1C16 | Low elevation (1dp) - Cards |
72
+ | `surfaceContainer` | #EEEFE3 | #1E201A | Default containers (3dp) |
73
+ | `surfaceContainerHigh` | #E8E9DE | #282B24 | High elevation (4dp) - Dialogs |
74
+ | `surfaceContainerHighest` | #E2E3D8 | #33362E | Highest elevation (5dp) |
75
+
76
+ ### Outline Colors
77
+ Used for borders and dividers.
78
+
79
+ | Token | Light Mode | Dark Mode | Usage |
80
+ |-------|-----------|-----------|-------|
81
+ | `outline` | #75796C | #8F9285 | Borders, dividers |
82
+ | `outlineVariant` | #C5C8BA | #44483D | Subtle borders |
83
+
84
+ ### Inverse Colors
85
+ Used for inverse color schemes (e.g., snackbars).
86
+
87
+ | Token | Light Mode | Dark Mode | Usage |
88
+ |-------|-----------|-----------|-------|
89
+ | `inverseSurface` | #2F312A | #E2E3D8 | Inverse backgrounds |
90
+ | `inverseOnSurface` | #F1F2E6 | #2F312A | Text on inverse surfaces |
91
+ | `inversePrimary` | #B1D18A | #4C662B | Primary color on inverse |
92
+
93
+ ### Background Colors
94
+ Used for page/app backgrounds.
95
+
96
+ | Token | Light Mode | Dark Mode | Usage |
97
+ |-------|-----------|-----------|-------|
98
+ | `background` | #F9FAEF | #12140E | Page background |
99
+ | `onBackground` | #1A1C16 | #E2E3D8 | Text on background |
100
+
101
+ ### Special Colors
102
+
103
+ | Token | Value | Usage |
104
+ |-------|-------|-------|
105
+ | `scrim` | #000000 | Overlay behind modals/dialogs |
106
+ | `shadow` | #000000 | Shadow color (rarely used directly) |
107
+
108
+ ## Usage in Code
109
+
110
+ ### Using Semantic Tokens in Components
111
+
112
+ ```typescript
113
+ import { css } from '@discourser/design-system/styled-system/css';
114
+
115
+ // ✅ Correct - Use semantic tokens
116
+ const container = css({
117
+ bg: 'surfaceContainerLow',
118
+ color: 'onSurface',
119
+ borderColor: 'outline'
120
+ });
121
+
122
+ const primaryAction = css({
123
+ bg: 'primary',
124
+ color: 'onPrimary'
125
+ });
126
+
127
+ const errorMessage = css({
128
+ bg: 'errorContainer',
129
+ color: 'onErrorContainer'
130
+ });
131
+ ```
132
+
133
+ ### Common Patterns
134
+
135
+ ```typescript
136
+ // Card backgrounds
137
+ bg: 'surfaceContainerLow' // For elevated cards
138
+ bg: 'surface' // For filled/outlined cards
139
+
140
+ // Text colors
141
+ color: 'onSurface' // Primary text
142
+ color: 'onSurfaceVariant' // Secondary text
143
+
144
+ // Borders
145
+ borderColor: 'outline' // Standard borders
146
+ borderColor: 'outlineVariant' // Subtle borders
147
+
148
+ // Interactive states
149
+ bg: 'primary' // Default
150
+ bg: 'primaryContainer' // Hover/focus
151
+ color: 'onPrimary' // Text on primary
152
+ ```
153
+
154
+ ## What NOT to Do
155
+
156
+ ```typescript
157
+ // ❌ NEVER use raw hex colors
158
+ const wrong = css({ bg: '#4C662B' });
159
+
160
+ // ❌ NEVER use RGB values
161
+ const wrong = css({ bg: 'rgb(76, 102, 43)' });
162
+
163
+ // ❌ NEVER hardcode light/dark colors
164
+ const wrong = css({ bg: mode === 'dark' ? '#000' : '#fff' });
165
+
166
+ // ✅ ALWAYS use semantic tokens
167
+ const correct = css({ bg: 'surface' });
168
+ ```
169
+
170
+ ## Color Combinations
171
+
172
+ Always pair colors with their corresponding "on" color for proper contrast:
173
+
174
+ | Background | Text/Icon Color | Use Case |
175
+ |-----------|----------------|----------|
176
+ | `primary` | `onPrimary` | Filled buttons |
177
+ | `primaryContainer` | `onPrimaryContainer` | Tonal buttons, chips |
178
+ | `surface` | `onSurface` | Default backgrounds |
179
+ | `surfaceVariant` | `onSurfaceVariant` | Alternate surfaces |
180
+ | `error` | `onError` | Error badges |
181
+ | `errorContainer` | `onErrorContainer` | Error messages |
182
+
183
+ ## Accessibility
184
+
185
+ All semantic color combinations meet WCAG 2.1 Level AA contrast requirements (4.5:1 for normal text, 3:1 for large text).
186
+
187
+ **Never override these combinations** unless you verify contrast ratios yourself.