@discourser/design-system 0.3.1 → 0.4.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.
@@ -0,0 +1,834 @@
1
+ # Tabs
2
+
3
+ **Purpose:** Navigation component for organizing content into separate views with smooth transitions and visual indicators, following Material Design 3 principles.
4
+
5
+ ## Import
6
+
7
+ ```typescript
8
+ import { Tabs } from '@discourser/design-system';
9
+ ```
10
+
11
+ ## Component Structure
12
+
13
+ The Tabs component is a **compound component** that follows the composition pattern. All parts must be used together:
14
+
15
+ | Component | Purpose | Required |
16
+ | ------------------- | ------------------------------------------- | ----------- |
17
+ | `Tabs.Root` | Container that manages tab state and layout | Yes |
18
+ | `Tabs.List` | Container for tab triggers | Yes |
19
+ | `Tabs.Trigger` | Individual tab button | Yes |
20
+ | `Tabs.Content` | Panel with content for each tab | Yes |
21
+ | `Tabs.Indicator` | Visual indicator showing active tab | Recommended |
22
+ | `Tabs.Context` | Access tabs state in custom components | Advanced |
23
+ | `Tabs.RootProvider` | Provide external tabs state | Advanced |
24
+
25
+ **Important:** Never use tab parts in isolation. They must be nested within `Tabs.Root`.
26
+
27
+ ## Variants
28
+
29
+ The Tabs component supports 3 Material Design 3 inspired variants:
30
+
31
+ | Variant | Visual Style | Usage | When to Use |
32
+ | ---------- | ---------------------------------- | ------------- | ---------------------------------------- |
33
+ | `line` | Underline indicator below tabs | Default style | Primary navigation, content switching |
34
+ | `subtle` | Background highlight on active tab | Soft emphasis | Secondary navigation, embedded tabs |
35
+ | `enclosed` | Pills with background container | High emphasis | Segmented controls, toggle-like behavior |
36
+
37
+ ### Visual Characteristics
38
+
39
+ - **line**: Border-bottom on list, sliding indicator line below active tab, primary color accent
40
+ - **subtle**: Rounded background behind active tab, subtle color palette, minimal borders
41
+ - **enclosed**: Contained pill-style buttons with shadow, gray background container, prominent selection
42
+
43
+ ## Sizes
44
+
45
+ | Size | Height | Padding (Horizontal) | Font Size | Usage |
46
+ | ---- | ------ | -------------------- | --------- | -------------------------------------------------- |
47
+ | `xs` | 32px | 12px | xs | Compact UI, dense layouts, small screens |
48
+ | `sm` | 36px | 14px | sm | Secondary navigation, sidebars |
49
+ | `md` | 40px | 16px | sm | Default, most use cases |
50
+ | `lg` | 44px | 18px | md | Touch-friendly, mobile-first, prominent navigation |
51
+
52
+ **Recommendation:** Use `md` for most cases. Use `lg` for mobile-first designs or primary navigation.
53
+
54
+ ## Props
55
+
56
+ ### Root Props
57
+
58
+ | Prop | Type | Default | Description |
59
+ | ---------------- | -------------------------------------- | -------------- | ----------------------------------------------------- |
60
+ | `defaultValue` | `string` | - | Initially selected tab |
61
+ | `value` | `string` | - | Controlled selected tab |
62
+ | `onValueChange` | `(details: { value: string }) => void` | - | Callback when tab selection changes |
63
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Tab list orientation |
64
+ | `activationMode` | `'automatic' \| 'manual'` | `'automatic'` | How tabs activate (on focus vs on click) |
65
+ | `loopFocus` | `boolean` | `true` | Whether arrow key navigation loops at ends |
66
+ | `variant` | `'line' \| 'subtle' \| 'enclosed'` | `'line'` | Visual style variant |
67
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Tab size |
68
+ | `fitted` | `boolean` | `false` | Whether tabs stretch to fill container width |
69
+ | `colorPalette` | `string` | - | Color palette for theming (e.g., 'primary', 'accent') |
70
+
71
+ ### Trigger Props
72
+
73
+ | Prop | Type | Default | Description |
74
+ | ---------- | ----------- | -------- | ----------------------------- |
75
+ | `value` | `string` | Required | Unique identifier for the tab |
76
+ | `disabled` | `boolean` | `false` | Disable this specific tab |
77
+ | `children` | `ReactNode` | Required | Tab label content |
78
+
79
+ ### Content Props
80
+
81
+ | Prop | Type | Default | Description |
82
+ | ---------- | ----------- | -------- | -------------------------------------- |
83
+ | `value` | `string` | Required | Must match corresponding Trigger value |
84
+ | `children` | `ReactNode` | Required | Tab panel content |
85
+
86
+ ### List Props
87
+
88
+ Standard HTML div props are supported.
89
+
90
+ ### Indicator Props
91
+
92
+ The indicator automatically follows the active tab. No props required.
93
+
94
+ ## Examples
95
+
96
+ ### Basic Usage
97
+
98
+ ```typescript
99
+ // Simple horizontal tabs (default)
100
+ <Tabs.Root colorPalette="primary" defaultValue="overview">
101
+ <Tabs.List>
102
+ <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
103
+ <Tabs.Trigger value="details">Details</Tabs.Trigger>
104
+ <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
105
+ <Tabs.Indicator />
106
+ </Tabs.List>
107
+
108
+ <Tabs.Content value="overview">
109
+ Overview content goes here
110
+ </Tabs.Content>
111
+ <Tabs.Content value="details">
112
+ Detailed information appears here
113
+ </Tabs.Content>
114
+ <Tabs.Content value="settings">
115
+ Settings panel content
116
+ </Tabs.Content>
117
+ </Tabs.Root>
118
+ ```
119
+
120
+ ### Controlled State
121
+
122
+ ```typescript
123
+ const [activeTab, setActiveTab] = useState('tab1');
124
+
125
+ <Tabs.Root
126
+ colorPalette="primary"
127
+ value={activeTab}
128
+ onValueChange={(details) => setActiveTab(details.value)}
129
+ >
130
+ <Tabs.List>
131
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
132
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
133
+ <Tabs.Indicator />
134
+ </Tabs.List>
135
+
136
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
137
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
138
+ </Tabs.Root>
139
+
140
+ <p>Current tab: {activeTab}</p>
141
+ ```
142
+
143
+ ### Variant Examples
144
+
145
+ ```typescript
146
+ // Line variant (default) - underlined indicator
147
+ <Tabs.Root colorPalette="primary" variant="line" defaultValue="tab1">
148
+ <Tabs.List>
149
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
150
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
151
+ <Tabs.Trigger value="tab3">Tab 3</Tabs.Trigger>
152
+ <Tabs.Indicator />
153
+ </Tabs.List>
154
+ <Tabs.Content value="tab1">Line variant content</Tabs.Content>
155
+ <Tabs.Content value="tab2">Second tab</Tabs.Content>
156
+ <Tabs.Content value="tab3">Third tab</Tabs.Content>
157
+ </Tabs.Root>
158
+
159
+ // Subtle variant - background highlight
160
+ <Tabs.Root colorPalette="primary" variant="subtle" defaultValue="tab1">
161
+ <Tabs.List>
162
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
163
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
164
+ <Tabs.Indicator />
165
+ </Tabs.List>
166
+ <Tabs.Content value="tab1">Subtle variant content</Tabs.Content>
167
+ <Tabs.Content value="tab2">Second tab</Tabs.Content>
168
+ </Tabs.Root>
169
+
170
+ // Enclosed variant - pill style
171
+ <Tabs.Root colorPalette="primary" variant="enclosed" defaultValue="tab1">
172
+ <Tabs.List>
173
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
174
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
175
+ <Tabs.Indicator />
176
+ </Tabs.List>
177
+ <Tabs.Content value="tab1">Enclosed variant content</Tabs.Content>
178
+ <Tabs.Content value="tab2">Second tab</Tabs.Content>
179
+ </Tabs.Root>
180
+ ```
181
+
182
+ ### Size Examples
183
+
184
+ ```typescript
185
+ // Extra small tabs
186
+ <Tabs.Root size="xs" colorPalette="primary" defaultValue="tab1">
187
+ <Tabs.List>
188
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
189
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
190
+ <Tabs.Indicator />
191
+ </Tabs.List>
192
+ <Tabs.Content value="tab1">Extra small content</Tabs.Content>
193
+ <Tabs.Content value="tab2">Second tab</Tabs.Content>
194
+ </Tabs.Root>
195
+
196
+ // Large tabs (touch-friendly)
197
+ <Tabs.Root size="lg" colorPalette="primary" defaultValue="tab1">
198
+ <Tabs.List>
199
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
200
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
201
+ <Tabs.Indicator />
202
+ </Tabs.List>
203
+ <Tabs.Content value="tab1">Large content</Tabs.Content>
204
+ <Tabs.Content value="tab2">Second tab</Tabs.Content>
205
+ </Tabs.Root>
206
+ ```
207
+
208
+ ### Vertical Orientation
209
+
210
+ ```typescript
211
+ <Tabs.Root orientation="vertical" colorPalette="primary" defaultValue="tab1">
212
+ <Tabs.List>
213
+ <Tabs.Trigger value="tab1">Dashboard</Tabs.Trigger>
214
+ <Tabs.Trigger value="tab2">Analytics</Tabs.Trigger>
215
+ <Tabs.Trigger value="tab3">Reports</Tabs.Trigger>
216
+ <Tabs.Indicator />
217
+ </Tabs.List>
218
+
219
+ <Tabs.Content value="tab1">Dashboard view</Tabs.Content>
220
+ <Tabs.Content value="tab2">Analytics charts</Tabs.Content>
221
+ <Tabs.Content value="tab3">Report tables</Tabs.Content>
222
+ </Tabs.Root>
223
+ ```
224
+
225
+ ### Fitted Tabs
226
+
227
+ ```typescript
228
+ // Tabs stretch to fill available width
229
+ <Tabs.Root fitted colorPalette="primary" defaultValue="tab1">
230
+ <Tabs.List>
231
+ <Tabs.Trigger value="tab1">Overview</Tabs.Trigger>
232
+ <Tabs.Trigger value="tab2">Details</Tabs.Trigger>
233
+ <Tabs.Trigger value="tab3">Settings</Tabs.Trigger>
234
+ <Tabs.Indicator />
235
+ </Tabs.List>
236
+
237
+ <Tabs.Content value="tab1">Overview content</Tabs.Content>
238
+ <Tabs.Content value="tab2">Details content</Tabs.Content>
239
+ <Tabs.Content value="tab3">Settings content</Tabs.Content>
240
+ </Tabs.Root>
241
+ ```
242
+
243
+ ### Disabled Tabs
244
+
245
+ ```typescript
246
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
247
+ <Tabs.List>
248
+ <Tabs.Trigger value="tab1">Active Tab</Tabs.Trigger>
249
+ <Tabs.Trigger value="tab2" disabled>
250
+ Disabled Tab
251
+ </Tabs.Trigger>
252
+ <Tabs.Trigger value="tab3">Another Active</Tabs.Trigger>
253
+ <Tabs.Indicator />
254
+ </Tabs.List>
255
+
256
+ <Tabs.Content value="tab1">Active content</Tabs.Content>
257
+ <Tabs.Content value="tab2">Inaccessible content</Tabs.Content>
258
+ <Tabs.Content value="tab3">Another active content</Tabs.Content>
259
+ </Tabs.Root>
260
+ ```
261
+
262
+ ### Manual Activation Mode
263
+
264
+ ```typescript
265
+ // Tabs activate on click/Enter, not on focus
266
+ <Tabs.Root activationMode="manual" colorPalette="primary" defaultValue="tab1">
267
+ <Tabs.List>
268
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
269
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
270
+ <Tabs.Trigger value="tab3">Tab 3</Tabs.Trigger>
271
+ <Tabs.Indicator />
272
+ </Tabs.List>
273
+
274
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
275
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
276
+ <Tabs.Content value="tab3">Content 3</Tabs.Content>
277
+ </Tabs.Root>
278
+ ```
279
+
280
+ ### Dynamic Tabs
281
+
282
+ ```typescript
283
+ const tabs = [
284
+ { id: 'home', label: 'Home', content: 'Welcome to the home page' },
285
+ { id: 'profile', label: 'Profile', content: 'Your profile information' },
286
+ { id: 'settings', label: 'Settings', content: 'Adjust your preferences' },
287
+ ];
288
+
289
+ <Tabs.Root colorPalette="primary" defaultValue={tabs[0].id}>
290
+ <Tabs.List>
291
+ {tabs.map((tab) => (
292
+ <Tabs.Trigger key={tab.id} value={tab.id}>
293
+ {tab.label}
294
+ </Tabs.Trigger>
295
+ ))}
296
+ <Tabs.Indicator />
297
+ </Tabs.List>
298
+
299
+ {tabs.map((tab) => (
300
+ <Tabs.Content key={tab.id} value={tab.id}>
301
+ {tab.content}
302
+ </Tabs.Content>
303
+ ))}
304
+ </Tabs.Root>
305
+ ```
306
+
307
+ ## Common Patterns
308
+
309
+ ### Profile Settings Navigation
310
+
311
+ ```typescript
312
+ <Tabs.Root colorPalette="primary" variant="line" defaultValue="account">
313
+ <Tabs.List>
314
+ <Tabs.Trigger value="account">Account</Tabs.Trigger>
315
+ <Tabs.Trigger value="security">Security</Tabs.Trigger>
316
+ <Tabs.Trigger value="notifications">Notifications</Tabs.Trigger>
317
+ <Tabs.Trigger value="billing">Billing</Tabs.Trigger>
318
+ <Tabs.Indicator />
319
+ </Tabs.List>
320
+
321
+ <Tabs.Content value="account">
322
+ <Box p="6">
323
+ <Heading size="lg">Account Settings</Heading>
324
+ <Input label="Username" defaultValue="johndoe" />
325
+ <Input label="Email" defaultValue="john@example.com" />
326
+ <Button>Save Changes</Button>
327
+ </Box>
328
+ </Tabs.Content>
329
+
330
+ <Tabs.Content value="security">
331
+ <Box p="6">
332
+ <Heading size="lg">Security Settings</Heading>
333
+ <Input type="password" label="Current Password" />
334
+ <Input type="password" label="New Password" />
335
+ <Button>Update Password</Button>
336
+ </Box>
337
+ </Tabs.Content>
338
+
339
+ {/* Other content panels */}
340
+ </Tabs.Root>
341
+ ```
342
+
343
+ ### Dashboard Views
344
+
345
+ ```typescript
346
+ <Tabs.Root
347
+ colorPalette="primary"
348
+ variant="enclosed"
349
+ size="lg"
350
+ defaultValue="overview"
351
+ >
352
+ <Tabs.List>
353
+ <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
354
+ <Tabs.Trigger value="analytics">Analytics</Tabs.Trigger>
355
+ <Tabs.Trigger value="reports">Reports</Tabs.Trigger>
356
+ <Tabs.Indicator />
357
+ </Tabs.List>
358
+
359
+ <Tabs.Content value="overview">
360
+ <Box p="6">
361
+ <h2>Dashboard Overview</h2>
362
+ {/* Stats cards, charts, etc. */}
363
+ </Box>
364
+ </Tabs.Content>
365
+
366
+ <Tabs.Content value="analytics">
367
+ <Box p="6">
368
+ <h2>Analytics</h2>
369
+ {/* Data visualizations */}
370
+ </Box>
371
+ </Tabs.Content>
372
+
373
+ <Tabs.Content value="reports">
374
+ <Box p="6">
375
+ <h2>Reports</h2>
376
+ {/* Report tables */}
377
+ </Box>
378
+ </Tabs.Content>
379
+ </Tabs.Root>
380
+ ```
381
+
382
+ ### Segmented Control
383
+
384
+ ```typescript
385
+ // Toggle-like behavior with enclosed variant
386
+ <Tabs.Root
387
+ colorPalette="primary"
388
+ variant="enclosed"
389
+ size="sm"
390
+ fitted
391
+ defaultValue="grid"
392
+ >
393
+ <Tabs.List>
394
+ <Tabs.Trigger value="list">List View</Tabs.Trigger>
395
+ <Tabs.Trigger value="grid">Grid View</Tabs.Trigger>
396
+ <Tabs.Indicator />
397
+ </Tabs.List>
398
+
399
+ <Tabs.Content value="list">
400
+ {/* List layout */}
401
+ </Tabs.Content>
402
+
403
+ <Tabs.Content value="grid">
404
+ {/* Grid layout */}
405
+ </Tabs.Content>
406
+ </Tabs.Root>
407
+ ```
408
+
409
+ ### Product Information Tabs
410
+
411
+ ```typescript
412
+ <Tabs.Root colorPalette="primary" variant="line" defaultValue="description">
413
+ <Tabs.List>
414
+ <Tabs.Trigger value="description">Description</Tabs.Trigger>
415
+ <Tabs.Trigger value="specs">Specifications</Tabs.Trigger>
416
+ <Tabs.Trigger value="reviews">Reviews (24)</Tabs.Trigger>
417
+ <Tabs.Indicator />
418
+ </Tabs.List>
419
+
420
+ <Tabs.Content value="description">
421
+ <Box p="4">
422
+ <p>Detailed product description with features and benefits...</p>
423
+ </Box>
424
+ </Tabs.Content>
425
+
426
+ <Tabs.Content value="specs">
427
+ <Box p="4">
428
+ <table>
429
+ <tr><td>Dimensions</td><td>10" x 8" x 2"</td></tr>
430
+ <tr><td>Weight</td><td>1.5 lbs</td></tr>
431
+ </table>
432
+ </Box>
433
+ </Tabs.Content>
434
+
435
+ <Tabs.Content value="reviews">
436
+ <Box p="4">
437
+ {/* Review list */}
438
+ </Box>
439
+ </Tabs.Content>
440
+ </Tabs.Root>
441
+ ```
442
+
443
+ ### With Icons
444
+
445
+ ```typescript
446
+ import { HomeIcon, UserIcon, SettingsIcon } from 'your-icon-library';
447
+
448
+ <Tabs.Root colorPalette="primary" defaultValue="home">
449
+ <Tabs.List>
450
+ <Tabs.Trigger value="home">
451
+ <HomeIcon />
452
+ Home
453
+ </Tabs.Trigger>
454
+ <Tabs.Trigger value="profile">
455
+ <UserIcon />
456
+ Profile
457
+ </Tabs.Trigger>
458
+ <Tabs.Trigger value="settings">
459
+ <SettingsIcon />
460
+ Settings
461
+ </Tabs.Trigger>
462
+ <Tabs.Indicator />
463
+ </Tabs.List>
464
+
465
+ <Tabs.Content value="home">Home content</Tabs.Content>
466
+ <Tabs.Content value="profile">Profile content</Tabs.Content>
467
+ <Tabs.Content value="settings">Settings content</Tabs.Content>
468
+ </Tabs.Root>
469
+ ```
470
+
471
+ ## DO NOT
472
+
473
+ ```typescript
474
+ // ❌ Don't use tab parts without Root
475
+ <Tabs.List>
476
+ <Tabs.Trigger value="tab1">Won't work</Tabs.Trigger>
477
+ </Tabs.List>
478
+
479
+ // ❌ Don't forget matching values
480
+ <Tabs.Root defaultValue="tab1">
481
+ <Tabs.List>
482
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
483
+ </Tabs.List>
484
+ <Tabs.Content value="different-value"> {/* Won't show! */}
485
+ Content
486
+ </Tabs.Content>
487
+ </Tabs.Root>
488
+
489
+ // ❌ Don't use duplicate values
490
+ <Tabs.Root defaultValue="tab1">
491
+ <Tabs.List>
492
+ <Tabs.Trigger value="same">Tab 1</Tabs.Trigger>
493
+ <Tabs.Trigger value="same">Tab 2</Tabs.Trigger> {/* Collision! */}
494
+ </Tabs.List>
495
+ </Tabs.Root>
496
+
497
+ // ❌ Don't nest interactive elements in Trigger
498
+ <Tabs.Trigger value="tab1">
499
+ <button>Nested button</button> {/* Breaks accessibility */}
500
+ </Tabs.Trigger>
501
+
502
+ // ❌ Don't omit Indicator (poor visual feedback)
503
+ <Tabs.List>
504
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
505
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
506
+ {/* Missing <Tabs.Indicator /> */}
507
+ </Tabs.List>
508
+
509
+ // ❌ Don't use tabs for sequential steps (use Stepper instead)
510
+ <Tabs.Root defaultValue="step1">
511
+ <Tabs.List>
512
+ <Tabs.Trigger value="step1">Step 1</Tabs.Trigger>
513
+ <Tabs.Trigger value="step2">Step 2</Tabs.Trigger>
514
+ <Tabs.Trigger value="step3">Step 3</Tabs.Trigger>
515
+ </Tabs.List>
516
+ {/* This should be a Stepper, not Tabs */}
517
+ </Tabs.Root>
518
+
519
+ // ❌ Don't use too many tabs (>7 becomes hard to scan)
520
+ <Tabs.List>
521
+ {/* 10+ tabs is overwhelming - consider dropdown or hierarchical nav */}
522
+ </Tabs.List>
523
+
524
+ // ✅ Use compound components properly
525
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
526
+ <Tabs.List>
527
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
528
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
529
+ <Tabs.Indicator />
530
+ </Tabs.List>
531
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
532
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
533
+ </Tabs.Root>
534
+ ```
535
+
536
+ ## Accessibility
537
+
538
+ The Tabs component follows WCAG 2.1 Level AA standards and implements WAI-ARIA Tabs Pattern:
539
+
540
+ - **Keyboard Navigation**:
541
+ - `Tab`: Focus into/out of tab list
542
+ - `ArrowRight` / `ArrowDown`: Next tab
543
+ - `ArrowLeft` / `ArrowUp`: Previous tab
544
+ - `Home`: First tab
545
+ - `End`: Last tab
546
+ - `Enter` / `Space`: Activate focused tab (manual mode)
547
+
548
+ - **ARIA Attributes**: Automatically managed
549
+ - `role="tablist"` on List
550
+ - `role="tab"` on Trigger
551
+ - `role="tabpanel"` on Content
552
+ - `aria-selected` on triggers (true/false)
553
+ - `aria-controls` links trigger to panel
554
+ - `aria-labelledby` links panel to trigger
555
+ - `aria-orientation` on vertical tab lists
556
+ - `aria-disabled` on disabled tabs
557
+
558
+ - **Focus Management**:
559
+ - Clear focus indicators on keyboard navigation
560
+ - Focus moves to active tab trigger when entering tab list
561
+ - Tab panel is focusable for screen reader access
562
+
563
+ - **Activation Modes**:
564
+ - **Automatic** (default): Tab activates on arrow key focus
565
+ - **Manual**: Tab activates only on Enter/Space (better for dynamic content)
566
+
567
+ ### Accessibility Best Practices
568
+
569
+ ```typescript
570
+ // ✅ Use descriptive tab labels
571
+ <Tabs.Trigger value="account">Account Settings</Tabs.Trigger>
572
+
573
+ // ✅ Use manual activation for tabs that load data
574
+ <Tabs.Root activationMode="manual" colorPalette="primary" defaultValue="tab1">
575
+ {/* Prevents triggering API calls on arrow key navigation */}
576
+ </Tabs.Root>
577
+
578
+ // ✅ Provide meaningful content in panels
579
+ <Tabs.Content value="overview">
580
+ <h2>Overview</h2>
581
+ <p>Well-structured content with headings...</p>
582
+ </Tabs.Content>
583
+
584
+ // ✅ Indicate disabled state clearly in label
585
+ <Tabs.Trigger value="premium" disabled>
586
+ Premium Features (Upgrade Required)
587
+ </Tabs.Trigger>
588
+
589
+ // ✅ Use icons with text labels, not icon-only
590
+ <Tabs.Trigger value="home">
591
+ <HomeIcon aria-hidden="true" />
592
+ Home
593
+ </Tabs.Trigger>
594
+ ```
595
+
596
+ ## Variant Selection Guide
597
+
598
+ | Scenario | Recommended Variant | Reasoning |
599
+ | -------------------- | -------------------- | -------------------------------------------------- |
600
+ | Primary navigation | `line` | Clear, familiar pattern for main content switching |
601
+ | Secondary navigation | `subtle` | Lower emphasis for nested or supplementary tabs |
602
+ | Segmented control | `enclosed` | Clear selection state, toggle-like behavior |
603
+ | Settings sections | `line` | Clean, organized appearance |
604
+ | Dashboard views | `line` or `enclosed` | Depends on design system aesthetics |
605
+ | Sidebar navigation | `subtle` | Integrates well with sidebar design |
606
+ | Toolbar controls | `enclosed` | Compact, clear selection |
607
+
608
+ ## State Behaviors
609
+
610
+ | State | Visual Change | Behavior |
611
+ | ------------ | --------------------------------- | -------------------------------------- |
612
+ | **Default** | Muted text color | Tab is inactive but selectable |
613
+ | **Selected** | Indicator visible, accent color | Tab panel content is shown |
614
+ | **Hover** | Subtle background or color change | Visual feedback on interactive element |
615
+ | **Focus** | Focus ring visible | Keyboard navigation indicator |
616
+ | **Disabled** | 38% opacity, cursor not-allowed | Tab cannot be activated |
617
+
618
+ ## Indicator Animation
619
+
620
+ The `Tabs.Indicator` smoothly animates between tabs using CSS transforms:
621
+
622
+ - **Horizontal**: Slides left/right with width adjustment
623
+ - **Vertical**: Slides up/down with height adjustment
624
+ - **Transition**: Smooth, native feel with GPU acceleration
625
+
626
+ ## Orientation Guidelines
627
+
628
+ ### Horizontal Tabs (Default)
629
+
630
+ - **Best for**: Primary navigation, most use cases
631
+ - **Layout**: Tabs arranged left-to-right
632
+ - **Responsiveness**: May need overflow handling on small screens
633
+ - **Common placement**: Top of content area
634
+
635
+ ### Vertical Tabs
636
+
637
+ - **Best for**: Sidebar navigation, settings with many sections
638
+ - **Layout**: Tabs arranged top-to-bottom
639
+ - **Responsiveness**: Works well on all screen sizes
640
+ - **Common placement**: Left or right side of content
641
+
642
+ ```typescript
643
+ // Responsive orientation
644
+ <Tabs.Root
645
+ orientation={{ base: 'horizontal', md: 'vertical' }}
646
+ colorPalette="primary"
647
+ defaultValue="tab1"
648
+ >
649
+ {/* Horizontal on mobile, vertical on desktop */}
650
+ </Tabs.Root>
651
+ ```
652
+
653
+ ## Responsive Considerations
654
+
655
+ ```typescript
656
+ // Responsive size
657
+ <Tabs.Root
658
+ size={{ base: 'lg', md: 'md' }}
659
+ colorPalette="primary"
660
+ defaultValue="tab1"
661
+ >
662
+ {/* Larger on mobile for touch, standard on desktop */}
663
+ </Tabs.Root>
664
+
665
+ // Responsive fitted
666
+ <Tabs.Root
667
+ fitted={{ base: true, md: false }}
668
+ colorPalette="primary"
669
+ defaultValue="tab1"
670
+ >
671
+ {/* Full width on mobile, auto width on desktop */}
672
+ </Tabs.Root>
673
+
674
+ // Handle overflow with scrolling
675
+ <Box overflowX="auto">
676
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
677
+ <Tabs.List>
678
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
679
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
680
+ <Tabs.Trigger value="tab3">Tab 3</Tabs.Trigger>
681
+ <Tabs.Trigger value="tab4">Tab 4</Tabs.Trigger>
682
+ <Tabs.Trigger value="tab5">Tab 5</Tabs.Trigger>
683
+ <Tabs.Indicator />
684
+ </Tabs.List>
685
+ {/* Content */}
686
+ </Tabs.Root>
687
+ </Box>
688
+ ```
689
+
690
+ ## Testing
691
+
692
+ When testing Tabs components:
693
+
694
+ ```typescript
695
+ import { render, screen } from '@testing-library/react';
696
+ import userEvent from '@testing-library/user-event';
697
+
698
+ test('tabs switch content on click', async () => {
699
+ render(
700
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
701
+ <Tabs.List>
702
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
703
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
704
+ <Tabs.Indicator />
705
+ </Tabs.List>
706
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
707
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
708
+ </Tabs.Root>
709
+ );
710
+
711
+ expect(screen.getByText('Content 1')).toBeVisible();
712
+ expect(screen.queryByText('Content 2')).not.toBeVisible();
713
+
714
+ await userEvent.click(screen.getByText('Tab 2'));
715
+
716
+ expect(screen.queryByText('Content 1')).not.toBeVisible();
717
+ expect(screen.getByText('Content 2')).toBeVisible();
718
+ });
719
+
720
+ test('disabled tab cannot be activated', async () => {
721
+ render(
722
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
723
+ <Tabs.List>
724
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
725
+ <Tabs.Trigger value="tab2" disabled>Tab 2</Tabs.Trigger>
726
+ <Tabs.Indicator />
727
+ </Tabs.List>
728
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
729
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
730
+ </Tabs.Root>
731
+ );
732
+
733
+ const disabledTab = screen.getByText('Tab 2');
734
+ await userEvent.click(disabledTab);
735
+
736
+ expect(screen.getByText('Content 1')).toBeVisible();
737
+ expect(screen.queryByText('Content 2')).not.toBeVisible();
738
+ expect(disabledTab).toHaveAttribute('aria-disabled', 'true');
739
+ });
740
+
741
+ test('keyboard navigation with arrow keys', async () => {
742
+ render(
743
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
744
+ <Tabs.List>
745
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
746
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
747
+ <Tabs.Trigger value="tab3">Tab 3</Tabs.Trigger>
748
+ <Tabs.Indicator />
749
+ </Tabs.List>
750
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
751
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
752
+ <Tabs.Content value="tab3">Content 3</Tabs.Content>
753
+ </Tabs.Root>
754
+ );
755
+
756
+ const tab1 = screen.getByText('Tab 1');
757
+ tab1.focus();
758
+
759
+ await userEvent.keyboard('{ArrowRight}');
760
+ expect(screen.getByText('Tab 2')).toHaveFocus();
761
+
762
+ await userEvent.keyboard('{ArrowRight}');
763
+ expect(screen.getByText('Tab 3')).toHaveFocus();
764
+
765
+ await userEvent.keyboard('{Home}');
766
+ expect(screen.getByText('Tab 1')).toHaveFocus();
767
+ });
768
+
769
+ test('controlled tabs update on value change', async () => {
770
+ const TestComponent = () => {
771
+ const [value, setValue] = useState('tab1');
772
+ return (
773
+ <>
774
+ <button onClick={() => setValue('tab2')}>Switch to Tab 2</button>
775
+ <Tabs.Root
776
+ colorPalette="primary"
777
+ value={value}
778
+ onValueChange={(d) => setValue(d.value)}
779
+ >
780
+ <Tabs.List>
781
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
782
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
783
+ <Tabs.Indicator />
784
+ </Tabs.List>
785
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
786
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
787
+ </Tabs.Root>
788
+ </>
789
+ );
790
+ };
791
+
792
+ render(<TestComponent />);
793
+ await userEvent.click(screen.getByText('Switch to Tab 2'));
794
+ expect(screen.getByText('Content 2')).toBeVisible();
795
+ });
796
+
797
+ test('manual activation mode requires explicit activation', async () => {
798
+ render(
799
+ <Tabs.Root
800
+ colorPalette="primary"
801
+ activationMode="manual"
802
+ defaultValue="tab1"
803
+ >
804
+ <Tabs.List>
805
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
806
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
807
+ <Tabs.Indicator />
808
+ </Tabs.List>
809
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
810
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
811
+ </Tabs.Root>
812
+ );
813
+
814
+ const tab1 = screen.getByText('Tab 1');
815
+ tab1.focus();
816
+
817
+ // Arrow key navigation doesn't activate tab in manual mode
818
+ await userEvent.keyboard('{ArrowRight}');
819
+ expect(screen.getByText('Tab 2')).toHaveFocus();
820
+ expect(screen.getByText('Content 1')).toBeVisible(); // Still tab 1 content
821
+
822
+ // Explicit Enter/Space activates tab
823
+ await userEvent.keyboard('{Enter}');
824
+ expect(screen.getByText('Content 2')).toBeVisible();
825
+ });
826
+ ```
827
+
828
+ ## Related Components
829
+
830
+ - **Accordion**: For progressive disclosure of stacked content
831
+ - **Menu**: For navigation or action menus with different interaction patterns
832
+ - **Breadcrumbs**: For hierarchical navigation
833
+ - **Stepper**: For sequential multi-step processes (not random access like tabs)
834
+ - **SegmentedControl**: Alternative for 2-3 mutually exclusive options