@discourser/design-system 0.3.1 → 0.5.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.
Files changed (36) hide show
  1. package/README.md +12 -4
  2. package/dist/styles.css +5126 -0
  3. package/guidelines/Guidelines.md +92 -41
  4. package/guidelines/components/accordion.md +732 -0
  5. package/guidelines/components/avatar.md +1015 -0
  6. package/guidelines/components/badge.md +728 -0
  7. package/guidelines/components/button.md +75 -40
  8. package/guidelines/components/card.md +84 -25
  9. package/guidelines/components/checkbox.md +671 -0
  10. package/guidelines/components/dialog.md +619 -31
  11. package/guidelines/components/drawer.md +1616 -0
  12. package/guidelines/components/heading.md +576 -0
  13. package/guidelines/components/icon-button.md +92 -37
  14. package/guidelines/components/input-addon.md +685 -0
  15. package/guidelines/components/input-group.md +830 -0
  16. package/guidelines/components/input.md +92 -37
  17. package/guidelines/components/popover.md +1271 -0
  18. package/guidelines/components/progress.md +836 -0
  19. package/guidelines/components/radio-group.md +852 -0
  20. package/guidelines/components/select.md +1662 -0
  21. package/guidelines/components/skeleton.md +802 -0
  22. package/guidelines/components/slider.md +911 -0
  23. package/guidelines/components/spinner.md +783 -0
  24. package/guidelines/components/switch.md +105 -38
  25. package/guidelines/components/tabs.md +1488 -0
  26. package/guidelines/components/textarea.md +495 -0
  27. package/guidelines/components/toast.md +784 -0
  28. package/guidelines/components/tooltip.md +912 -0
  29. package/guidelines/design-tokens/colors.md +309 -72
  30. package/guidelines/design-tokens/elevation.md +615 -45
  31. package/guidelines/design-tokens/spacing.md +654 -74
  32. package/guidelines/design-tokens/typography.md +432 -50
  33. package/guidelines/overview-components.md +60 -8
  34. package/guidelines/overview-imports.md +314 -0
  35. package/guidelines/overview-patterns.md +3852 -0
  36. package/package.json +4 -2
@@ -0,0 +1,1488 @@
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
+ ## When to Use This Component
6
+
7
+ Use Tabs when you need to **organize related content into separate, mutually exclusive views** where users switch between them without leaving the page.
8
+
9
+ ### Decision Tree
10
+
11
+ | Scenario | Use Tabs? | Alternative | Reasoning |
12
+ | ---------------------------------------------------- | --------- | ------------------------ | ------------------------------------------------------------ |
13
+ | Switching between related views (Dashboard sections) | ✅ Yes | - | Tabs provide clear navigation between distinct content areas |
14
+ | Organizing product info (Details, Reviews, Specs) | ✅ Yes | - | Perfect for grouping related but separate information |
15
+ | User needs to see multiple sections at once | ❌ No | Accordion (multiple) | Tabs show one view at a time |
16
+ | Progressive disclosure of content (FAQs) | ❌ No | Accordion | Accordion is better for showing/hiding sections |
17
+ | Sequential steps in a process | ❌ No | Stepper | Stepper shows progress through ordered steps |
18
+ | More than 7-8 tabs needed | ❌ No | Dropdown menu or Sidebar | Too many tabs become hard to navigate |
19
+
20
+ ### Component Comparison
21
+
22
+ ```typescript
23
+ // ✅ Tabs - Switching between related views
24
+ <Tabs.Root colorPalette="primary" defaultValue="overview">
25
+ <Tabs.List>
26
+ <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
27
+ <Tabs.Trigger value="analytics">Analytics</Tabs.Trigger>
28
+ <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
29
+ <Tabs.Indicator />
30
+ </Tabs.List>
31
+ <Tabs.Content value="overview">
32
+ <DashboardOverview />
33
+ </Tabs.Content>
34
+ <Tabs.Content value="analytics">
35
+ <AnalyticsView />
36
+ </Tabs.Content>
37
+ <Tabs.Content value="settings">
38
+ <SettingsPanel />
39
+ </Tabs.Content>
40
+ </Tabs.Root>
41
+
42
+ // ❌ Don't use Tabs when users need to see multiple sections - Use Accordion
43
+ <Tabs.Root defaultValue="section1">
44
+ <Tabs.List>
45
+ <Tabs.Trigger value="section1">FAQ 1</Tabs.Trigger>
46
+ <Tabs.Trigger value="section2">FAQ 2</Tabs.Trigger>
47
+ {/* User might want to see multiple FAQs at once */}
48
+ </Tabs.List>
49
+ </Tabs.Root>
50
+
51
+ // ✅ Better: Use Accordion for collapsible sections
52
+ <Accordion.Root multiple>
53
+ <Accordion.Item value="faq1">
54
+ <Accordion.ItemTrigger>
55
+ FAQ 1
56
+ <Accordion.ItemIndicator />
57
+ </Accordion.ItemTrigger>
58
+ <Accordion.ItemContent>Answer 1</Accordion.ItemContent>
59
+ </Accordion.Item>
60
+ <Accordion.Item value="faq2">
61
+ <Accordion.ItemTrigger>
62
+ FAQ 2
63
+ <Accordion.ItemIndicator />
64
+ </Accordion.ItemTrigger>
65
+ <Accordion.ItemContent>Answer 2</Accordion.ItemContent>
66
+ </Accordion.Item>
67
+ </Accordion.Root>
68
+
69
+ // ❌ Don't use Tabs for sequential steps - Use Stepper
70
+ <Tabs.Root defaultValue="step1">
71
+ <Tabs.List>
72
+ <Tabs.Trigger value="step1">Step 1</Tabs.Trigger>
73
+ <Tabs.Trigger value="step2">Step 2</Tabs.Trigger>
74
+ <Tabs.Trigger value="step3">Step 3</Tabs.Trigger>
75
+ </Tabs.List>
76
+ {/* Steps have an order - tabs don't convey sequence */}
77
+ </Tabs.Root>
78
+
79
+ // ✅ Better: Use Stepper for multi-step processes
80
+ <Stepper value={currentStep} onValueChange={setCurrentStep}>
81
+ <Stepper.List>
82
+ <Stepper.Item index={0}>Information</Stepper.Item>
83
+ <Stepper.Item index={1}>Payment</Stepper.Item>
84
+ <Stepper.Item index={2}>Confirmation</Stepper.Item>
85
+ </Stepper.List>
86
+ <Stepper.Content index={0}>Step 1 content</Stepper.Content>
87
+ <Stepper.Content index={1}>Step 2 content</Stepper.Content>
88
+ <Stepper.Content index={2}>Step 3 content</Stepper.Content>
89
+ </Stepper>
90
+ ```
91
+
92
+ ## Import
93
+
94
+ ```typescript
95
+ import { Tabs } from '@discourser/design-system';
96
+ ```
97
+
98
+ ## Component Structure
99
+
100
+ The Tabs component is a **compound component** that follows the composition pattern. All parts must be used together:
101
+
102
+ | Component | Purpose | Required |
103
+ | ------------------- | ------------------------------------------- | ----------- |
104
+ | `Tabs.Root` | Container that manages tab state and layout | Yes |
105
+ | `Tabs.List` | Container for tab triggers | Yes |
106
+ | `Tabs.Trigger` | Individual tab button | Yes |
107
+ | `Tabs.Content` | Panel with content for each tab | Yes |
108
+ | `Tabs.Indicator` | Visual indicator showing active tab | Recommended |
109
+ | `Tabs.Context` | Access tabs state in custom components | Advanced |
110
+ | `Tabs.RootProvider` | Provide external tabs state | Advanced |
111
+
112
+ **Important:** Never use tab parts in isolation. They must be nested within `Tabs.Root`.
113
+
114
+ ## Variants
115
+
116
+ The Tabs component supports 3 Material Design 3 inspired variants:
117
+
118
+ | Variant | Visual Style | Usage | When to Use |
119
+ | ---------- | ---------------------------------- | ------------- | ---------------------------------------- |
120
+ | `line` | Underline indicator below tabs | Default style | Primary navigation, content switching |
121
+ | `subtle` | Background highlight on active tab | Soft emphasis | Secondary navigation, embedded tabs |
122
+ | `enclosed` | Pills with background container | High emphasis | Segmented controls, toggle-like behavior |
123
+
124
+ ### Visual Characteristics
125
+
126
+ - **line**: Border-bottom on list, sliding indicator line below active tab, primary color accent
127
+ - **subtle**: Rounded background behind active tab, subtle color palette, minimal borders
128
+ - **enclosed**: Contained pill-style buttons with shadow, gray background container, prominent selection
129
+
130
+ ## Sizes
131
+
132
+ | Size | Height | Padding (Horizontal) | Font Size | Usage |
133
+ | ---- | ------ | -------------------- | --------- | -------------------------------------------------- |
134
+ | `xs` | 32px | 12px | xs | Compact UI, dense layouts, small screens |
135
+ | `sm` | 36px | 14px | sm | Secondary navigation, sidebars |
136
+ | `md` | 40px | 16px | sm | Default, most use cases |
137
+ | `lg` | 44px | 18px | md | Touch-friendly, mobile-first, prominent navigation |
138
+
139
+ **Recommendation:** Use `md` for most cases. Use `lg` for mobile-first designs or primary navigation.
140
+
141
+ ## Props
142
+
143
+ ### Root Props
144
+
145
+ | Prop | Type | Default | Description |
146
+ | ---------------- | -------------------------------------- | -------------- | ----------------------------------------------------- |
147
+ | `defaultValue` | `string` | - | Initially selected tab |
148
+ | `value` | `string` | - | Controlled selected tab |
149
+ | `onValueChange` | `(details: { value: string }) => void` | - | Callback when tab selection changes |
150
+ | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Tab list orientation |
151
+ | `activationMode` | `'automatic' \| 'manual'` | `'automatic'` | How tabs activate (on focus vs on click) |
152
+ | `loopFocus` | `boolean` | `true` | Whether arrow key navigation loops at ends |
153
+ | `variant` | `'line' \| 'subtle' \| 'enclosed'` | `'line'` | Visual style variant |
154
+ | `size` | `'xs' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Tab size |
155
+ | `fitted` | `boolean` | `false` | Whether tabs stretch to fill container width |
156
+ | `colorPalette` | `string` | - | Color palette for theming (e.g., 'primary', 'accent') |
157
+
158
+ ### Trigger Props
159
+
160
+ | Prop | Type | Default | Description |
161
+ | ---------- | ----------- | -------- | ----------------------------- |
162
+ | `value` | `string` | Required | Unique identifier for the tab |
163
+ | `disabled` | `boolean` | `false` | Disable this specific tab |
164
+ | `children` | `ReactNode` | Required | Tab label content |
165
+
166
+ ### Content Props
167
+
168
+ | Prop | Type | Default | Description |
169
+ | ---------- | ----------- | -------- | -------------------------------------- |
170
+ | `value` | `string` | Required | Must match corresponding Trigger value |
171
+ | `children` | `ReactNode` | Required | Tab panel content |
172
+
173
+ ### List Props
174
+
175
+ Standard HTML div props are supported.
176
+
177
+ ### Indicator Props
178
+
179
+ The indicator automatically follows the active tab. No props required.
180
+
181
+ ## Examples
182
+
183
+ ### Basic Usage
184
+
185
+ ```typescript
186
+ // Simple horizontal tabs (default)
187
+ <Tabs.Root colorPalette="primary" defaultValue="overview">
188
+ <Tabs.List>
189
+ <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
190
+ <Tabs.Trigger value="details">Details</Tabs.Trigger>
191
+ <Tabs.Trigger value="settings">Settings</Tabs.Trigger>
192
+ <Tabs.Indicator />
193
+ </Tabs.List>
194
+
195
+ <Tabs.Content value="overview">
196
+ Overview content goes here
197
+ </Tabs.Content>
198
+ <Tabs.Content value="details">
199
+ Detailed information appears here
200
+ </Tabs.Content>
201
+ <Tabs.Content value="settings">
202
+ Settings panel content
203
+ </Tabs.Content>
204
+ </Tabs.Root>
205
+ ```
206
+
207
+ ### Controlled State
208
+
209
+ ```typescript
210
+ const [activeTab, setActiveTab] = useState('tab1');
211
+
212
+ <Tabs.Root
213
+ colorPalette="primary"
214
+ value={activeTab}
215
+ onValueChange={(details) => setActiveTab(details.value)}
216
+ >
217
+ <Tabs.List>
218
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
219
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
220
+ <Tabs.Indicator />
221
+ </Tabs.List>
222
+
223
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
224
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
225
+ </Tabs.Root>
226
+
227
+ <p>Current tab: {activeTab}</p>
228
+ ```
229
+
230
+ ### Variant Examples
231
+
232
+ ```typescript
233
+ // Line variant (default) - underlined indicator
234
+ <Tabs.Root colorPalette="primary" variant="line" defaultValue="tab1">
235
+ <Tabs.List>
236
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
237
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
238
+ <Tabs.Trigger value="tab3">Tab 3</Tabs.Trigger>
239
+ <Tabs.Indicator />
240
+ </Tabs.List>
241
+ <Tabs.Content value="tab1">Line variant content</Tabs.Content>
242
+ <Tabs.Content value="tab2">Second tab</Tabs.Content>
243
+ <Tabs.Content value="tab3">Third tab</Tabs.Content>
244
+ </Tabs.Root>
245
+
246
+ // Subtle variant - background highlight
247
+ <Tabs.Root colorPalette="primary" variant="subtle" defaultValue="tab1">
248
+ <Tabs.List>
249
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
250
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
251
+ <Tabs.Indicator />
252
+ </Tabs.List>
253
+ <Tabs.Content value="tab1">Subtle variant content</Tabs.Content>
254
+ <Tabs.Content value="tab2">Second tab</Tabs.Content>
255
+ </Tabs.Root>
256
+
257
+ // Enclosed variant - pill style
258
+ <Tabs.Root colorPalette="primary" variant="enclosed" defaultValue="tab1">
259
+ <Tabs.List>
260
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
261
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
262
+ <Tabs.Indicator />
263
+ </Tabs.List>
264
+ <Tabs.Content value="tab1">Enclosed variant content</Tabs.Content>
265
+ <Tabs.Content value="tab2">Second tab</Tabs.Content>
266
+ </Tabs.Root>
267
+ ```
268
+
269
+ ### Size Examples
270
+
271
+ ```typescript
272
+ // Extra small tabs
273
+ <Tabs.Root size="xs" colorPalette="primary" defaultValue="tab1">
274
+ <Tabs.List>
275
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
276
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
277
+ <Tabs.Indicator />
278
+ </Tabs.List>
279
+ <Tabs.Content value="tab1">Extra small content</Tabs.Content>
280
+ <Tabs.Content value="tab2">Second tab</Tabs.Content>
281
+ </Tabs.Root>
282
+
283
+ // Large tabs (touch-friendly)
284
+ <Tabs.Root size="lg" colorPalette="primary" defaultValue="tab1">
285
+ <Tabs.List>
286
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
287
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
288
+ <Tabs.Indicator />
289
+ </Tabs.List>
290
+ <Tabs.Content value="tab1">Large content</Tabs.Content>
291
+ <Tabs.Content value="tab2">Second tab</Tabs.Content>
292
+ </Tabs.Root>
293
+ ```
294
+
295
+ ### Vertical Orientation
296
+
297
+ ```typescript
298
+ <Tabs.Root orientation="vertical" colorPalette="primary" defaultValue="tab1">
299
+ <Tabs.List>
300
+ <Tabs.Trigger value="tab1">Dashboard</Tabs.Trigger>
301
+ <Tabs.Trigger value="tab2">Analytics</Tabs.Trigger>
302
+ <Tabs.Trigger value="tab3">Reports</Tabs.Trigger>
303
+ <Tabs.Indicator />
304
+ </Tabs.List>
305
+
306
+ <Tabs.Content value="tab1">Dashboard view</Tabs.Content>
307
+ <Tabs.Content value="tab2">Analytics charts</Tabs.Content>
308
+ <Tabs.Content value="tab3">Report tables</Tabs.Content>
309
+ </Tabs.Root>
310
+ ```
311
+
312
+ ### Fitted Tabs
313
+
314
+ ```typescript
315
+ // Tabs stretch to fill available width
316
+ <Tabs.Root fitted colorPalette="primary" defaultValue="tab1">
317
+ <Tabs.List>
318
+ <Tabs.Trigger value="tab1">Overview</Tabs.Trigger>
319
+ <Tabs.Trigger value="tab2">Details</Tabs.Trigger>
320
+ <Tabs.Trigger value="tab3">Settings</Tabs.Trigger>
321
+ <Tabs.Indicator />
322
+ </Tabs.List>
323
+
324
+ <Tabs.Content value="tab1">Overview content</Tabs.Content>
325
+ <Tabs.Content value="tab2">Details content</Tabs.Content>
326
+ <Tabs.Content value="tab3">Settings content</Tabs.Content>
327
+ </Tabs.Root>
328
+ ```
329
+
330
+ ### Disabled Tabs
331
+
332
+ ```typescript
333
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
334
+ <Tabs.List>
335
+ <Tabs.Trigger value="tab1">Active Tab</Tabs.Trigger>
336
+ <Tabs.Trigger value="tab2" disabled>
337
+ Disabled Tab
338
+ </Tabs.Trigger>
339
+ <Tabs.Trigger value="tab3">Another Active</Tabs.Trigger>
340
+ <Tabs.Indicator />
341
+ </Tabs.List>
342
+
343
+ <Tabs.Content value="tab1">Active content</Tabs.Content>
344
+ <Tabs.Content value="tab2">Inaccessible content</Tabs.Content>
345
+ <Tabs.Content value="tab3">Another active content</Tabs.Content>
346
+ </Tabs.Root>
347
+ ```
348
+
349
+ ### Manual Activation Mode
350
+
351
+ ```typescript
352
+ // Tabs activate on click/Enter, not on focus
353
+ <Tabs.Root activationMode="manual" colorPalette="primary" defaultValue="tab1">
354
+ <Tabs.List>
355
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
356
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
357
+ <Tabs.Trigger value="tab3">Tab 3</Tabs.Trigger>
358
+ <Tabs.Indicator />
359
+ </Tabs.List>
360
+
361
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
362
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
363
+ <Tabs.Content value="tab3">Content 3</Tabs.Content>
364
+ </Tabs.Root>
365
+ ```
366
+
367
+ ### Dynamic Tabs
368
+
369
+ ```typescript
370
+ const tabs = [
371
+ { id: 'home', label: 'Home', content: 'Welcome to the home page' },
372
+ { id: 'profile', label: 'Profile', content: 'Your profile information' },
373
+ { id: 'settings', label: 'Settings', content: 'Adjust your preferences' },
374
+ ];
375
+
376
+ <Tabs.Root colorPalette="primary" defaultValue={tabs[0].id}>
377
+ <Tabs.List>
378
+ {tabs.map((tab) => (
379
+ <Tabs.Trigger key={tab.id} value={tab.id}>
380
+ {tab.label}
381
+ </Tabs.Trigger>
382
+ ))}
383
+ <Tabs.Indicator />
384
+ </Tabs.List>
385
+
386
+ {tabs.map((tab) => (
387
+ <Tabs.Content key={tab.id} value={tab.id}>
388
+ {tab.content}
389
+ </Tabs.Content>
390
+ ))}
391
+ </Tabs.Root>
392
+ ```
393
+
394
+ ## Common Patterns
395
+
396
+ ### Profile Settings Navigation
397
+
398
+ ```typescript
399
+ <Tabs.Root colorPalette="primary" variant="line" defaultValue="account">
400
+ <Tabs.List>
401
+ <Tabs.Trigger value="account">Account</Tabs.Trigger>
402
+ <Tabs.Trigger value="security">Security</Tabs.Trigger>
403
+ <Tabs.Trigger value="notifications">Notifications</Tabs.Trigger>
404
+ <Tabs.Trigger value="billing">Billing</Tabs.Trigger>
405
+ <Tabs.Indicator />
406
+ </Tabs.List>
407
+
408
+ <Tabs.Content value="account">
409
+ <Box p="6">
410
+ <Heading size="lg">Account Settings</Heading>
411
+ <Input label="Username" defaultValue="johndoe" />
412
+ <Input label="Email" defaultValue="john@example.com" />
413
+ <Button>Save Changes</Button>
414
+ </Box>
415
+ </Tabs.Content>
416
+
417
+ <Tabs.Content value="security">
418
+ <Box p="6">
419
+ <Heading size="lg">Security Settings</Heading>
420
+ <Input type="password" label="Current Password" />
421
+ <Input type="password" label="New Password" />
422
+ <Button>Update Password</Button>
423
+ </Box>
424
+ </Tabs.Content>
425
+
426
+ {/* Other content panels */}
427
+ </Tabs.Root>
428
+ ```
429
+
430
+ ### Dashboard Views
431
+
432
+ ```typescript
433
+ <Tabs.Root
434
+ colorPalette="primary"
435
+ variant="enclosed"
436
+ size="lg"
437
+ defaultValue="overview"
438
+ >
439
+ <Tabs.List>
440
+ <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
441
+ <Tabs.Trigger value="analytics">Analytics</Tabs.Trigger>
442
+ <Tabs.Trigger value="reports">Reports</Tabs.Trigger>
443
+ <Tabs.Indicator />
444
+ </Tabs.List>
445
+
446
+ <Tabs.Content value="overview">
447
+ <Box p="6">
448
+ <h2>Dashboard Overview</h2>
449
+ {/* Stats cards, charts, etc. */}
450
+ </Box>
451
+ </Tabs.Content>
452
+
453
+ <Tabs.Content value="analytics">
454
+ <Box p="6">
455
+ <h2>Analytics</h2>
456
+ {/* Data visualizations */}
457
+ </Box>
458
+ </Tabs.Content>
459
+
460
+ <Tabs.Content value="reports">
461
+ <Box p="6">
462
+ <h2>Reports</h2>
463
+ {/* Report tables */}
464
+ </Box>
465
+ </Tabs.Content>
466
+ </Tabs.Root>
467
+ ```
468
+
469
+ ### Segmented Control
470
+
471
+ ```typescript
472
+ // Toggle-like behavior with enclosed variant
473
+ <Tabs.Root
474
+ colorPalette="primary"
475
+ variant="enclosed"
476
+ size="sm"
477
+ fitted
478
+ defaultValue="grid"
479
+ >
480
+ <Tabs.List>
481
+ <Tabs.Trigger value="list">List View</Tabs.Trigger>
482
+ <Tabs.Trigger value="grid">Grid View</Tabs.Trigger>
483
+ <Tabs.Indicator />
484
+ </Tabs.List>
485
+
486
+ <Tabs.Content value="list">
487
+ {/* List layout */}
488
+ </Tabs.Content>
489
+
490
+ <Tabs.Content value="grid">
491
+ {/* Grid layout */}
492
+ </Tabs.Content>
493
+ </Tabs.Root>
494
+ ```
495
+
496
+ ### Product Information Tabs
497
+
498
+ ```typescript
499
+ <Tabs.Root colorPalette="primary" variant="line" defaultValue="description">
500
+ <Tabs.List>
501
+ <Tabs.Trigger value="description">Description</Tabs.Trigger>
502
+ <Tabs.Trigger value="specs">Specifications</Tabs.Trigger>
503
+ <Tabs.Trigger value="reviews">Reviews (24)</Tabs.Trigger>
504
+ <Tabs.Indicator />
505
+ </Tabs.List>
506
+
507
+ <Tabs.Content value="description">
508
+ <Box p="4">
509
+ <p>Detailed product description with features and benefits...</p>
510
+ </Box>
511
+ </Tabs.Content>
512
+
513
+ <Tabs.Content value="specs">
514
+ <Box p="4">
515
+ <table>
516
+ <tr><td>Dimensions</td><td>10" x 8" x 2"</td></tr>
517
+ <tr><td>Weight</td><td>1.5 lbs</td></tr>
518
+ </table>
519
+ </Box>
520
+ </Tabs.Content>
521
+
522
+ <Tabs.Content value="reviews">
523
+ <Box p="4">
524
+ {/* Review list */}
525
+ </Box>
526
+ </Tabs.Content>
527
+ </Tabs.Root>
528
+ ```
529
+
530
+ ### With Icons
531
+
532
+ ```typescript
533
+ import { HomeIcon, UserIcon, SettingsIcon } from 'your-icon-library';
534
+
535
+ <Tabs.Root colorPalette="primary" defaultValue="home">
536
+ <Tabs.List>
537
+ <Tabs.Trigger value="home">
538
+ <HomeIcon />
539
+ Home
540
+ </Tabs.Trigger>
541
+ <Tabs.Trigger value="profile">
542
+ <UserIcon />
543
+ Profile
544
+ </Tabs.Trigger>
545
+ <Tabs.Trigger value="settings">
546
+ <SettingsIcon />
547
+ Settings
548
+ </Tabs.Trigger>
549
+ <Tabs.Indicator />
550
+ </Tabs.List>
551
+
552
+ <Tabs.Content value="home">Home content</Tabs.Content>
553
+ <Tabs.Content value="profile">Profile content</Tabs.Content>
554
+ <Tabs.Content value="settings">Settings content</Tabs.Content>
555
+ </Tabs.Root>
556
+ ```
557
+
558
+ ## Edge Cases
559
+
560
+ This section covers common edge cases and how to handle them properly.
561
+
562
+ ### Dynamic Tabs - Adding/Removing at Runtime
563
+
564
+ **Scenario:** Tabs need to be added or removed dynamically based on user actions or application state.
565
+
566
+ **Solution:**
567
+
568
+ ```typescript
569
+ interface Tab {
570
+ id: string;
571
+ label: string;
572
+ content: string;
573
+ closable?: boolean;
574
+ }
575
+
576
+ const [tabs, setTabs] = useState<Tab[]>([
577
+ { id: 'tab1', label: 'Home', content: 'Home content', closable: false },
578
+ { id: 'tab2', label: 'Profile', content: 'Profile content', closable: true },
579
+ ]);
580
+ const [activeTab, setActiveTab] = useState('tab1');
581
+
582
+ const addTab = () => {
583
+ const newTabId = `tab${Date.now()}`;
584
+ const newTab: Tab = {
585
+ id: newTabId,
586
+ label: `New Tab ${tabs.length + 1}`,
587
+ content: `Content for new tab`,
588
+ closable: true,
589
+ };
590
+ setTabs([...tabs, newTab]);
591
+ setActiveTab(newTabId); // Activate newly created tab
592
+ };
593
+
594
+ const closeTab = (tabId: string) => {
595
+ // Prevent closing if it's the last tab
596
+ if (tabs.length <= 1) return;
597
+
598
+ const tabIndex = tabs.findIndex((t) => t.id === tabId);
599
+ const newTabs = tabs.filter((t) => t.id !== tabId);
600
+ setTabs(newTabs);
601
+
602
+ // If closing active tab, switch to adjacent tab
603
+ if (activeTab === tabId) {
604
+ const newActiveIndex = Math.min(tabIndex, newTabs.length - 1);
605
+ setActiveTab(newTabs[newActiveIndex].id);
606
+ }
607
+ };
608
+
609
+ <div>
610
+ <Tabs.Root
611
+ colorPalette="primary"
612
+ value={activeTab}
613
+ onValueChange={(details) => setActiveTab(details.value)}
614
+ >
615
+ <div className={css({ display: 'flex', alignItems: 'center', gap: '2', mb: '2' })}>
616
+ <Tabs.List className={css({ flex: 1 })}>
617
+ {tabs.map((tab) => (
618
+ <Tabs.Trigger key={tab.id} value={tab.id}>
619
+ <span>{tab.label}</span>
620
+ {tab.closable && (
621
+ <button
622
+ className={css({
623
+ ml: '2',
624
+ p: '1',
625
+ borderRadius: 'sm',
626
+ _hover: { bg: 'gray.a3' },
627
+ })}
628
+ onClick={(e) => {
629
+ e.stopPropagation(); // Prevent tab activation
630
+ closeTab(tab.id);
631
+ }}
632
+ aria-label={`Close ${tab.label}`}
633
+ >
634
+ ×
635
+ </button>
636
+ )}
637
+ </Tabs.Trigger>
638
+ ))}
639
+ <Tabs.Indicator />
640
+ </Tabs.List>
641
+
642
+ <Button size="sm" onClick={addTab} variant="outlined">
643
+ + Add Tab
644
+ </Button>
645
+ </div>
646
+
647
+ {tabs.map((tab) => (
648
+ <Tabs.Content key={tab.id} value={tab.id}>
649
+ <div className={css({ p: '4' })}>{tab.content}</div>
650
+ </Tabs.Content>
651
+ ))}
652
+ </Tabs.Root>
653
+ </div>
654
+ ```
655
+
656
+ **Best practices:**
657
+
658
+ - Always maintain at least one tab to prevent empty state
659
+ - Switch to adjacent tab when closing the active tab
660
+ - Stop event propagation on close buttons to prevent tab activation
661
+ - Provide clear visual indicators for closable tabs
662
+ - Consider confirming before closing tabs with unsaved changes
663
+
664
+ ---
665
+
666
+ ### Too Many Tabs - Handling Overflow
667
+
668
+ **Scenario:** When there are too many tabs to fit horizontally, implement scrolling or alternative navigation patterns.
669
+
670
+ **Solution:**
671
+
672
+ ```typescript
673
+ const manyTabs = Array.from({ length: 12 }, (_, i) => ({
674
+ id: `tab${i + 1}`,
675
+ label: `Tab ${i + 1}`,
676
+ content: `Content for tab ${i + 1}`,
677
+ }));
678
+
679
+ const [activeTab, setActiveTab] = useState('tab1');
680
+
681
+ <Tabs.Root
682
+ colorPalette="primary"
683
+ value={activeTab}
684
+ onValueChange={(details) => setActiveTab(details.value)}
685
+ >
686
+ {/* Horizontal scroll container */}
687
+ <div
688
+ className={css({
689
+ overflowX: 'auto',
690
+ overflowY: 'hidden',
691
+ WebkitOverflowScrolling: 'touch', // Smooth scrolling on iOS
692
+ scrollbarWidth: 'thin', // Firefox
693
+ '&::-webkit-scrollbar': {
694
+ height: '6px',
695
+ },
696
+ '&::-webkit-scrollbar-thumb': {
697
+ bg: 'gray.a5',
698
+ borderRadius: 'full',
699
+ },
700
+ })}
701
+ >
702
+ <Tabs.List className={css({ minWidth: 'max-content' })}>
703
+ {manyTabs.map((tab) => (
704
+ <Tabs.Trigger
705
+ key={tab.id}
706
+ value={tab.id}
707
+ className={css({ whiteSpace: 'nowrap', flexShrink: 0 })}
708
+ >
709
+ {tab.label}
710
+ </Tabs.Trigger>
711
+ ))}
712
+ <Tabs.Indicator />
713
+ </Tabs.List>
714
+ </div>
715
+
716
+ {manyTabs.map((tab) => (
717
+ <Tabs.Content key={tab.id} value={tab.id}>
718
+ <div className={css({ p: '4' })}>{tab.content}</div>
719
+ </Tabs.Content>
720
+ ))}
721
+ </Tabs.Root>
722
+
723
+ {/* Alternative: Dropdown for overflow tabs */}
724
+ <Tabs.Root
725
+ colorPalette="primary"
726
+ value={activeTab}
727
+ onValueChange={(details) => setActiveTab(details.value)}
728
+ >
729
+ <div className={css({ display: 'flex', alignItems: 'center' })}>
730
+ <Tabs.List>
731
+ {/* Show first 5 tabs */}
732
+ {manyTabs.slice(0, 5).map((tab) => (
733
+ <Tabs.Trigger key={tab.id} value={tab.id}>
734
+ {tab.label}
735
+ </Tabs.Trigger>
736
+ ))}
737
+ <Tabs.Indicator />
738
+ </Tabs.List>
739
+
740
+ {/* Dropdown for remaining tabs */}
741
+ {manyTabs.length > 5 && (
742
+ <Select.Root
743
+ items={manyTabs.slice(5)}
744
+ itemToValue={(item) => item.id}
745
+ itemToString={(item) => item.label}
746
+ value={[activeTab]}
747
+ onValueChange={(details) => setActiveTab(details.value[0])}
748
+ >
749
+ <Select.Control>
750
+ <Select.Trigger>
751
+ <Select.ValueText placeholder="More..." />
752
+ </Select.Trigger>
753
+ </Select.Control>
754
+ <Select.Positioner>
755
+ <Select.Content>
756
+ <Select.List>
757
+ {manyTabs.slice(5).map((tab) => (
758
+ <Select.Item key={tab.id} item={tab}>
759
+ <Select.ItemText>{tab.label}</Select.ItemText>
760
+ </Select.Item>
761
+ ))}
762
+ </Select.List>
763
+ </Select.Content>
764
+ </Select.Positioner>
765
+ </Select.Root>
766
+ )}
767
+ </div>
768
+
769
+ {manyTabs.map((tab) => (
770
+ <Tabs.Content key={tab.id} value={tab.id}>
771
+ <div className={css({ p: '4' })}>{tab.content}</div>
772
+ </Tabs.Content>
773
+ ))}
774
+ </Tabs.Root>
775
+ ```
776
+
777
+ **Best practices:**
778
+
779
+ - Limit visible tabs to 7-8 maximum for usability
780
+ - Use horizontal scrolling with clear scroll indicators
781
+ - Consider a dropdown menu for overflow tabs
782
+ - Add visual cues (shadows/gradients) to indicate more tabs
783
+ - On mobile, consider switching to a different navigation pattern
784
+
785
+ ---
786
+
787
+ ### Disabled Tabs - Conditional Access
788
+
789
+ **Scenario:** Some tabs should be visible but not accessible due to permissions, incomplete prerequisites, or application state.
790
+
791
+ **Solution:**
792
+
793
+ ```typescript
794
+ interface TabConfig {
795
+ id: string;
796
+ label: string;
797
+ content: string;
798
+ disabled: boolean;
799
+ disabledReason?: string;
800
+ }
801
+
802
+ const [hasPermission, setHasPermission] = useState(false);
803
+ const [completedStep1, setCompletedStep1] = useState(false);
804
+
805
+ const tabConfigs: TabConfig[] = [
806
+ {
807
+ id: 'overview',
808
+ label: 'Overview',
809
+ content: 'Overview content available to all',
810
+ disabled: false,
811
+ },
812
+ {
813
+ id: 'analytics',
814
+ label: 'Analytics',
815
+ content: 'Analytics content',
816
+ disabled: !hasPermission,
817
+ disabledReason: 'Requires Pro subscription',
818
+ },
819
+ {
820
+ id: 'advanced',
821
+ label: 'Advanced',
822
+ content: 'Advanced settings',
823
+ disabled: !completedStep1,
824
+ disabledReason: 'Complete overview first',
825
+ },
826
+ ];
827
+
828
+ const [activeTab, setActiveTab] = useState('overview');
829
+ const [tooltipTab, setTooltipTab] = useState<string | null>(null);
830
+
831
+ <div>
832
+ {/* Controls for demo */}
833
+ <div className={css({ mb: '4', p: '3', bg: 'gray.a2', borderRadius: 'md' })}>
834
+ <label>
835
+ <input
836
+ type="checkbox"
837
+ checked={hasPermission}
838
+ onChange={(e) => setHasPermission(e.target.checked)}
839
+ />
840
+ <span className={css({ ml: '2' })}>Has Pro subscription</span>
841
+ </label>
842
+ <label className={css({ ml: '4' })}>
843
+ <input
844
+ type="checkbox"
845
+ checked={completedStep1}
846
+ onChange={(e) => setCompletedStep1(e.target.checked)}
847
+ />
848
+ <span className={css({ ml: '2' })}>Completed overview</span>
849
+ </label>
850
+ </div>
851
+
852
+ <Tabs.Root
853
+ colorPalette="primary"
854
+ value={activeTab}
855
+ onValueChange={(details) => setActiveTab(details.value)}
856
+ >
857
+ <Tabs.List>
858
+ {tabConfigs.map((tab) => (
859
+ <div
860
+ key={tab.id}
861
+ className={css({ position: 'relative' })}
862
+ onMouseEnter={() => tab.disabled && setTooltipTab(tab.id)}
863
+ onMouseLeave={() => setTooltipTab(null)}
864
+ >
865
+ <Tabs.Trigger value={tab.id} disabled={tab.disabled}>
866
+ {tab.label}
867
+ {tab.disabled && (
868
+ <span className={css({ ml: '1', fontSize: 'xs', color: 'fg.muted' })}>
869
+ 🔒
870
+ </span>
871
+ )}
872
+ </Tabs.Trigger>
873
+
874
+ {/* Tooltip for disabled tabs */}
875
+ {tab.disabled && tooltipTab === tab.id && tab.disabledReason && (
876
+ <div
877
+ className={css({
878
+ position: 'absolute',
879
+ top: 'full',
880
+ left: '50%',
881
+ transform: 'translateX(-50%)',
882
+ mt: '2',
883
+ p: '2',
884
+ bg: 'gray.12',
885
+ color: 'white',
886
+ fontSize: 'xs',
887
+ borderRadius: 'sm',
888
+ whiteSpace: 'nowrap',
889
+ zIndex: 10,
890
+ })}
891
+ >
892
+ {tab.disabledReason}
893
+ </div>
894
+ )}
895
+ </div>
896
+ ))}
897
+ <Tabs.Indicator />
898
+ </Tabs.List>
899
+
900
+ {tabConfigs.map((tab) => (
901
+ <Tabs.Content key={tab.id} value={tab.id}>
902
+ <div className={css({ p: '4' })}>{tab.content}</div>
903
+ </Tabs.Content>
904
+ ))}
905
+ </Tabs.Root>
906
+ </div>
907
+ ```
908
+
909
+ **Best practices:**
910
+
911
+ - Clearly indicate why a tab is disabled with tooltips or labels
912
+ - Use lock icons or visual indicators for disabled tabs
913
+ - Keep disabled tabs visible for discoverability
914
+ - Provide actionable steps to enable disabled tabs when possible
915
+ - Ensure `aria-disabled` is properly communicated to screen readers
916
+
917
+ ---
918
+
919
+ ### Async Tab Content - Lazy Loading
920
+
921
+ **Scenario:** Tab content is expensive to render or requires data fetching, so it should only load when the tab is first selected.
922
+
923
+ **Solution:**
924
+
925
+ ```typescript
926
+ interface TabData {
927
+ id: string;
928
+ label: string;
929
+ loaded: boolean;
930
+ loading: boolean;
931
+ data: any | null;
932
+ error: string | null;
933
+ }
934
+
935
+ const [activeTab, setActiveTab] = useState('overview');
936
+ const [tabsData, setTabsData] = useState<Record<string, TabData>>({
937
+ overview: { id: 'overview', label: 'Overview', loaded: true, loading: false, data: {}, error: null },
938
+ analytics: { id: 'analytics', label: 'Analytics', loaded: false, loading: false, data: null, error: null },
939
+ reports: { id: 'reports', label: 'Reports', loaded: false, loading: false, data: null, error: null },
940
+ });
941
+
942
+ // Load data when tab becomes active
943
+ useEffect(() => {
944
+ const currentTab = tabsData[activeTab];
945
+
946
+ if (!currentTab.loaded && !currentTab.loading) {
947
+ // Start loading
948
+ setTabsData((prev) => ({
949
+ ...prev,
950
+ [activeTab]: { ...prev[activeTab], loading: true },
951
+ }));
952
+
953
+ // Simulate async data fetch
954
+ fetch(`/api/${activeTab}`)
955
+ .then((res) => res.json())
956
+ .then((data) => {
957
+ setTabsData((prev) => ({
958
+ ...prev,
959
+ [activeTab]: {
960
+ ...prev[activeTab],
961
+ loaded: true,
962
+ loading: false,
963
+ data,
964
+ error: null,
965
+ },
966
+ }));
967
+ })
968
+ .catch((error) => {
969
+ setTabsData((prev) => ({
970
+ ...prev,
971
+ [activeTab]: {
972
+ ...prev[activeTab],
973
+ loading: false,
974
+ error: error.message,
975
+ },
976
+ }));
977
+ });
978
+ }
979
+ }, [activeTab]);
980
+
981
+ <Tabs.Root
982
+ colorPalette="primary"
983
+ value={activeTab}
984
+ onValueChange={(details) => setActiveTab(details.value)}
985
+ >
986
+ <Tabs.List>
987
+ {Object.values(tabsData).map((tab) => (
988
+ <Tabs.Trigger key={tab.id} value={tab.id}>
989
+ {tab.label}
990
+ {tab.loading && (
991
+ <span className={css({ ml: '2' })}>
992
+ <Spinner size="xs" />
993
+ </span>
994
+ )}
995
+ </Tabs.Trigger>
996
+ ))}
997
+ <Tabs.Indicator />
998
+ </Tabs.List>
999
+
1000
+ {Object.values(tabsData).map((tab) => (
1001
+ <Tabs.Content key={tab.id} value={tab.id}>
1002
+ <div className={css({ p: '4', minHeight: '200px' })}>
1003
+ {tab.loading && (
1004
+ <div className={css({ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '2' })}>
1005
+ <Spinner />
1006
+ <p className={css({ color: 'fg.muted' })}>Loading {tab.label.toLowerCase()}...</p>
1007
+ </div>
1008
+ )}
1009
+
1010
+ {tab.error && (
1011
+ <div className={css({ textAlign: 'center', color: 'error.fg' })}>
1012
+ <p>Failed to load {tab.label.toLowerCase()}</p>
1013
+ <p className={css({ fontSize: 'sm', mt: '2' })}>{tab.error}</p>
1014
+ </div>
1015
+ )}
1016
+
1017
+ {tab.loaded && !tab.loading && !tab.error && (
1018
+ <div>
1019
+ <h3>{tab.label} Content</h3>
1020
+ <pre>{JSON.stringify(tab.data, null, 2)}</pre>
1021
+ </div>
1022
+ )}
1023
+ </div>
1024
+ </Tabs.Content>
1025
+ ))}
1026
+ </Tabs.Root>
1027
+ ```
1028
+
1029
+ **Best practices:**
1030
+
1031
+ - Show loading indicators in both the tab trigger and content area
1032
+ - Cache loaded data to avoid refetching when switching back
1033
+ - Handle errors gracefully with retry options
1034
+ - Set minimum heights to prevent layout shifts during loading
1035
+ - Use `activationMode="manual"` for expensive operations to prevent accidental loads
1036
+
1037
+ ---
1038
+
1039
+ ### Keyboard Navigation - Advanced Controls
1040
+
1041
+ **Scenario:** Implementing comprehensive keyboard navigation including Home, End, and proper arrow key behavior with looping.
1042
+
1043
+ **Solution:**
1044
+
1045
+ ```typescript
1046
+ const tabs = [
1047
+ { id: 'home', label: 'Home', content: 'Home content' },
1048
+ { id: 'profile', label: 'Profile', content: 'Profile content' },
1049
+ { id: 'settings', label: 'Settings', content: 'Settings content' },
1050
+ { id: 'help', label: 'Help', content: 'Help content' },
1051
+ ];
1052
+
1053
+ const [activeTab, setActiveTab] = useState('home');
1054
+ const [lastInteraction, setLastInteraction] = useState<'mouse' | 'keyboard'>('mouse');
1055
+
1056
+ <div>
1057
+ {/* Keyboard shortcuts legend */}
1058
+ <div className={css({ mb: '3', p: '2', bg: 'gray.a2', borderRadius: 'sm', fontSize: 'sm' })}>
1059
+ <strong>Keyboard shortcuts:</strong> Arrow keys to navigate, Home/End for first/last tab,
1060
+ Enter/Space to activate (manual mode)
1061
+ </div>
1062
+
1063
+ <Tabs.Root
1064
+ colorPalette="primary"
1065
+ value={activeTab}
1066
+ onValueChange={(details) => {
1067
+ setActiveTab(details.value);
1068
+ }}
1069
+ // Enable keyboard navigation
1070
+ loopFocus={true} // Arrow keys loop from last to first
1071
+ activationMode="automatic" // Change to "manual" for explicit Enter/Space activation
1072
+ onFocusChange={(details) => {
1073
+ // Track interaction type for analytics or styling
1074
+ setLastInteraction('keyboard');
1075
+ }}
1076
+ >
1077
+ <Tabs.List
1078
+ onMouseDown={() => setLastInteraction('mouse')}
1079
+ className={css({
1080
+ // Add visual indicator for keyboard focus
1081
+ '& [data-focus]': {
1082
+ outline: lastInteraction === 'keyboard' ? '2px solid' : 'none',
1083
+ outlineColor: 'primary.9',
1084
+ outlineOffset: '2px',
1085
+ },
1086
+ })}
1087
+ >
1088
+ {tabs.map((tab) => (
1089
+ <Tabs.Trigger key={tab.id} value={tab.id}>
1090
+ {tab.label}
1091
+ </Tabs.Trigger>
1092
+ ))}
1093
+ <Tabs.Indicator />
1094
+ </Tabs.List>
1095
+
1096
+ {tabs.map((tab) => (
1097
+ <Tabs.Content
1098
+ key={tab.id}
1099
+ value={tab.id}
1100
+ // Make content focusable for screen readers
1101
+ tabIndex={0}
1102
+ >
1103
+ <div className={css({ p: '4' })}>{tab.content}</div>
1104
+ </Tabs.Content>
1105
+ ))}
1106
+ </Tabs.Root>
1107
+
1108
+ <div className={css({ mt: '3', fontSize: 'sm', color: 'fg.muted' })}>
1109
+ Last interaction: {lastInteraction}
1110
+ </div>
1111
+ </div>
1112
+ ```
1113
+
1114
+ **Best practices:**
1115
+
1116
+ - Enable `loopFocus` for intuitive circular navigation
1117
+ - Use `activationMode="manual"` for tabs with expensive content
1118
+ - Show clear focus indicators for keyboard navigation
1119
+ - Make tab content focusable for screen reader access
1120
+ - Test with keyboard-only navigation to ensure full accessibility
1121
+ - Document keyboard shortcuts for users
1122
+
1123
+ ---
1124
+
1125
+ ## DO NOT
1126
+
1127
+ ```typescript
1128
+ // ❌ Don't use tab parts without Root
1129
+ <Tabs.List>
1130
+ <Tabs.Trigger value="tab1">Won't work</Tabs.Trigger>
1131
+ </Tabs.List>
1132
+
1133
+ // ❌ Don't forget matching values
1134
+ <Tabs.Root defaultValue="tab1">
1135
+ <Tabs.List>
1136
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
1137
+ </Tabs.List>
1138
+ <Tabs.Content value="different-value"> {/* Won't show! */}
1139
+ Content
1140
+ </Tabs.Content>
1141
+ </Tabs.Root>
1142
+
1143
+ // ❌ Don't use duplicate values
1144
+ <Tabs.Root defaultValue="tab1">
1145
+ <Tabs.List>
1146
+ <Tabs.Trigger value="same">Tab 1</Tabs.Trigger>
1147
+ <Tabs.Trigger value="same">Tab 2</Tabs.Trigger> {/* Collision! */}
1148
+ </Tabs.List>
1149
+ </Tabs.Root>
1150
+
1151
+ // ❌ Don't nest interactive elements in Trigger
1152
+ <Tabs.Trigger value="tab1">
1153
+ <button>Nested button</button> {/* Breaks accessibility */}
1154
+ </Tabs.Trigger>
1155
+
1156
+ // ❌ Don't omit Indicator (poor visual feedback)
1157
+ <Tabs.List>
1158
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
1159
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
1160
+ {/* Missing <Tabs.Indicator /> */}
1161
+ </Tabs.List>
1162
+
1163
+ // ❌ Don't use tabs for sequential steps (use Stepper instead)
1164
+ <Tabs.Root defaultValue="step1">
1165
+ <Tabs.List>
1166
+ <Tabs.Trigger value="step1">Step 1</Tabs.Trigger>
1167
+ <Tabs.Trigger value="step2">Step 2</Tabs.Trigger>
1168
+ <Tabs.Trigger value="step3">Step 3</Tabs.Trigger>
1169
+ </Tabs.List>
1170
+ {/* This should be a Stepper, not Tabs */}
1171
+ </Tabs.Root>
1172
+
1173
+ // ❌ Don't use too many tabs (>7 becomes hard to scan)
1174
+ <Tabs.List>
1175
+ {/* 10+ tabs is overwhelming - consider dropdown or hierarchical nav */}
1176
+ </Tabs.List>
1177
+
1178
+ // ✅ Use compound components properly
1179
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
1180
+ <Tabs.List>
1181
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
1182
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
1183
+ <Tabs.Indicator />
1184
+ </Tabs.List>
1185
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
1186
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
1187
+ </Tabs.Root>
1188
+ ```
1189
+
1190
+ ## Accessibility
1191
+
1192
+ The Tabs component follows WCAG 2.1 Level AA standards and implements WAI-ARIA Tabs Pattern:
1193
+
1194
+ - **Keyboard Navigation**:
1195
+ - `Tab`: Focus into/out of tab list
1196
+ - `ArrowRight` / `ArrowDown`: Next tab
1197
+ - `ArrowLeft` / `ArrowUp`: Previous tab
1198
+ - `Home`: First tab
1199
+ - `End`: Last tab
1200
+ - `Enter` / `Space`: Activate focused tab (manual mode)
1201
+
1202
+ - **ARIA Attributes**: Automatically managed
1203
+ - `role="tablist"` on List
1204
+ - `role="tab"` on Trigger
1205
+ - `role="tabpanel"` on Content
1206
+ - `aria-selected` on triggers (true/false)
1207
+ - `aria-controls` links trigger to panel
1208
+ - `aria-labelledby` links panel to trigger
1209
+ - `aria-orientation` on vertical tab lists
1210
+ - `aria-disabled` on disabled tabs
1211
+
1212
+ - **Focus Management**:
1213
+ - Clear focus indicators on keyboard navigation
1214
+ - Focus moves to active tab trigger when entering tab list
1215
+ - Tab panel is focusable for screen reader access
1216
+
1217
+ - **Activation Modes**:
1218
+ - **Automatic** (default): Tab activates on arrow key focus
1219
+ - **Manual**: Tab activates only on Enter/Space (better for dynamic content)
1220
+
1221
+ ### Accessibility Best Practices
1222
+
1223
+ ```typescript
1224
+ // ✅ Use descriptive tab labels
1225
+ <Tabs.Trigger value="account">Account Settings</Tabs.Trigger>
1226
+
1227
+ // ✅ Use manual activation for tabs that load data
1228
+ <Tabs.Root activationMode="manual" colorPalette="primary" defaultValue="tab1">
1229
+ {/* Prevents triggering API calls on arrow key navigation */}
1230
+ </Tabs.Root>
1231
+
1232
+ // ✅ Provide meaningful content in panels
1233
+ <Tabs.Content value="overview">
1234
+ <h2>Overview</h2>
1235
+ <p>Well-structured content with headings...</p>
1236
+ </Tabs.Content>
1237
+
1238
+ // ✅ Indicate disabled state clearly in label
1239
+ <Tabs.Trigger value="premium" disabled>
1240
+ Premium Features (Upgrade Required)
1241
+ </Tabs.Trigger>
1242
+
1243
+ // ✅ Use icons with text labels, not icon-only
1244
+ <Tabs.Trigger value="home">
1245
+ <HomeIcon aria-hidden="true" />
1246
+ Home
1247
+ </Tabs.Trigger>
1248
+ ```
1249
+
1250
+ ## Variant Selection Guide
1251
+
1252
+ | Scenario | Recommended Variant | Reasoning |
1253
+ | -------------------- | -------------------- | -------------------------------------------------- |
1254
+ | Primary navigation | `line` | Clear, familiar pattern for main content switching |
1255
+ | Secondary navigation | `subtle` | Lower emphasis for nested or supplementary tabs |
1256
+ | Segmented control | `enclosed` | Clear selection state, toggle-like behavior |
1257
+ | Settings sections | `line` | Clean, organized appearance |
1258
+ | Dashboard views | `line` or `enclosed` | Depends on design system aesthetics |
1259
+ | Sidebar navigation | `subtle` | Integrates well with sidebar design |
1260
+ | Toolbar controls | `enclosed` | Compact, clear selection |
1261
+
1262
+ ## State Behaviors
1263
+
1264
+ | State | Visual Change | Behavior |
1265
+ | ------------ | --------------------------------- | -------------------------------------- |
1266
+ | **Default** | Muted text color | Tab is inactive but selectable |
1267
+ | **Selected** | Indicator visible, accent color | Tab panel content is shown |
1268
+ | **Hover** | Subtle background or color change | Visual feedback on interactive element |
1269
+ | **Focus** | Focus ring visible | Keyboard navigation indicator |
1270
+ | **Disabled** | 38% opacity, cursor not-allowed | Tab cannot be activated |
1271
+
1272
+ ## Indicator Animation
1273
+
1274
+ The `Tabs.Indicator` smoothly animates between tabs using CSS transforms:
1275
+
1276
+ - **Horizontal**: Slides left/right with width adjustment
1277
+ - **Vertical**: Slides up/down with height adjustment
1278
+ - **Transition**: Smooth, native feel with GPU acceleration
1279
+
1280
+ ## Orientation Guidelines
1281
+
1282
+ ### Horizontal Tabs (Default)
1283
+
1284
+ - **Best for**: Primary navigation, most use cases
1285
+ - **Layout**: Tabs arranged left-to-right
1286
+ - **Responsiveness**: May need overflow handling on small screens
1287
+ - **Common placement**: Top of content area
1288
+
1289
+ ### Vertical Tabs
1290
+
1291
+ - **Best for**: Sidebar navigation, settings with many sections
1292
+ - **Layout**: Tabs arranged top-to-bottom
1293
+ - **Responsiveness**: Works well on all screen sizes
1294
+ - **Common placement**: Left or right side of content
1295
+
1296
+ ```typescript
1297
+ // Responsive orientation
1298
+ <Tabs.Root
1299
+ orientation={{ base: 'horizontal', md: 'vertical' }}
1300
+ colorPalette="primary"
1301
+ defaultValue="tab1"
1302
+ >
1303
+ {/* Horizontal on mobile, vertical on desktop */}
1304
+ </Tabs.Root>
1305
+ ```
1306
+
1307
+ ## Responsive Considerations
1308
+
1309
+ ```typescript
1310
+ // Responsive size
1311
+ <Tabs.Root
1312
+ size={{ base: 'lg', md: 'md' }}
1313
+ colorPalette="primary"
1314
+ defaultValue="tab1"
1315
+ >
1316
+ {/* Larger on mobile for touch, standard on desktop */}
1317
+ </Tabs.Root>
1318
+
1319
+ // Responsive fitted
1320
+ <Tabs.Root
1321
+ fitted={{ base: true, md: false }}
1322
+ colorPalette="primary"
1323
+ defaultValue="tab1"
1324
+ >
1325
+ {/* Full width on mobile, auto width on desktop */}
1326
+ </Tabs.Root>
1327
+
1328
+ // Handle overflow with scrolling
1329
+ <Box overflowX="auto">
1330
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
1331
+ <Tabs.List>
1332
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
1333
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
1334
+ <Tabs.Trigger value="tab3">Tab 3</Tabs.Trigger>
1335
+ <Tabs.Trigger value="tab4">Tab 4</Tabs.Trigger>
1336
+ <Tabs.Trigger value="tab5">Tab 5</Tabs.Trigger>
1337
+ <Tabs.Indicator />
1338
+ </Tabs.List>
1339
+ {/* Content */}
1340
+ </Tabs.Root>
1341
+ </Box>
1342
+ ```
1343
+
1344
+ ## Testing
1345
+
1346
+ When testing Tabs components:
1347
+
1348
+ ```typescript
1349
+ import { render, screen } from '@testing-library/react';
1350
+ import userEvent from '@testing-library/user-event';
1351
+
1352
+ test('tabs switch content on click', async () => {
1353
+ render(
1354
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
1355
+ <Tabs.List>
1356
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
1357
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
1358
+ <Tabs.Indicator />
1359
+ </Tabs.List>
1360
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
1361
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
1362
+ </Tabs.Root>
1363
+ );
1364
+
1365
+ expect(screen.getByText('Content 1')).toBeVisible();
1366
+ expect(screen.queryByText('Content 2')).not.toBeVisible();
1367
+
1368
+ await userEvent.click(screen.getByText('Tab 2'));
1369
+
1370
+ expect(screen.queryByText('Content 1')).not.toBeVisible();
1371
+ expect(screen.getByText('Content 2')).toBeVisible();
1372
+ });
1373
+
1374
+ test('disabled tab cannot be activated', async () => {
1375
+ render(
1376
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
1377
+ <Tabs.List>
1378
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
1379
+ <Tabs.Trigger value="tab2" disabled>Tab 2</Tabs.Trigger>
1380
+ <Tabs.Indicator />
1381
+ </Tabs.List>
1382
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
1383
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
1384
+ </Tabs.Root>
1385
+ );
1386
+
1387
+ const disabledTab = screen.getByText('Tab 2');
1388
+ await userEvent.click(disabledTab);
1389
+
1390
+ expect(screen.getByText('Content 1')).toBeVisible();
1391
+ expect(screen.queryByText('Content 2')).not.toBeVisible();
1392
+ expect(disabledTab).toHaveAttribute('aria-disabled', 'true');
1393
+ });
1394
+
1395
+ test('keyboard navigation with arrow keys', async () => {
1396
+ render(
1397
+ <Tabs.Root colorPalette="primary" defaultValue="tab1">
1398
+ <Tabs.List>
1399
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
1400
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
1401
+ <Tabs.Trigger value="tab3">Tab 3</Tabs.Trigger>
1402
+ <Tabs.Indicator />
1403
+ </Tabs.List>
1404
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
1405
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
1406
+ <Tabs.Content value="tab3">Content 3</Tabs.Content>
1407
+ </Tabs.Root>
1408
+ );
1409
+
1410
+ const tab1 = screen.getByText('Tab 1');
1411
+ tab1.focus();
1412
+
1413
+ await userEvent.keyboard('{ArrowRight}');
1414
+ expect(screen.getByText('Tab 2')).toHaveFocus();
1415
+
1416
+ await userEvent.keyboard('{ArrowRight}');
1417
+ expect(screen.getByText('Tab 3')).toHaveFocus();
1418
+
1419
+ await userEvent.keyboard('{Home}');
1420
+ expect(screen.getByText('Tab 1')).toHaveFocus();
1421
+ });
1422
+
1423
+ test('controlled tabs update on value change', async () => {
1424
+ const TestComponent = () => {
1425
+ const [value, setValue] = useState('tab1');
1426
+ return (
1427
+ <>
1428
+ <button onClick={() => setValue('tab2')}>Switch to Tab 2</button>
1429
+ <Tabs.Root
1430
+ colorPalette="primary"
1431
+ value={value}
1432
+ onValueChange={(d) => setValue(d.value)}
1433
+ >
1434
+ <Tabs.List>
1435
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
1436
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
1437
+ <Tabs.Indicator />
1438
+ </Tabs.List>
1439
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
1440
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
1441
+ </Tabs.Root>
1442
+ </>
1443
+ );
1444
+ };
1445
+
1446
+ render(<TestComponent />);
1447
+ await userEvent.click(screen.getByText('Switch to Tab 2'));
1448
+ expect(screen.getByText('Content 2')).toBeVisible();
1449
+ });
1450
+
1451
+ test('manual activation mode requires explicit activation', async () => {
1452
+ render(
1453
+ <Tabs.Root
1454
+ colorPalette="primary"
1455
+ activationMode="manual"
1456
+ defaultValue="tab1"
1457
+ >
1458
+ <Tabs.List>
1459
+ <Tabs.Trigger value="tab1">Tab 1</Tabs.Trigger>
1460
+ <Tabs.Trigger value="tab2">Tab 2</Tabs.Trigger>
1461
+ <Tabs.Indicator />
1462
+ </Tabs.List>
1463
+ <Tabs.Content value="tab1">Content 1</Tabs.Content>
1464
+ <Tabs.Content value="tab2">Content 2</Tabs.Content>
1465
+ </Tabs.Root>
1466
+ );
1467
+
1468
+ const tab1 = screen.getByText('Tab 1');
1469
+ tab1.focus();
1470
+
1471
+ // Arrow key navigation doesn't activate tab in manual mode
1472
+ await userEvent.keyboard('{ArrowRight}');
1473
+ expect(screen.getByText('Tab 2')).toHaveFocus();
1474
+ expect(screen.getByText('Content 1')).toBeVisible(); // Still tab 1 content
1475
+
1476
+ // Explicit Enter/Space activates tab
1477
+ await userEvent.keyboard('{Enter}');
1478
+ expect(screen.getByText('Content 2')).toBeVisible();
1479
+ });
1480
+ ```
1481
+
1482
+ ## Related Components
1483
+
1484
+ - **Accordion**: For progressive disclosure of stacked content
1485
+ - **Menu**: For navigation or action menus with different interaction patterns
1486
+ - **Breadcrumbs**: For hierarchical navigation
1487
+ - **Stepper**: For sequential multi-step processes (not random access like tabs)
1488
+ - **SegmentedControl**: Alternative for 2-3 mutually exclusive options