@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.
- package/guidelines/Guidelines.md +120 -13
- package/guidelines/components/accordion.md +639 -0
- package/guidelines/components/avatar.md +945 -0
- package/guidelines/components/badge.md +667 -0
- package/guidelines/components/checkbox.md +583 -0
- package/guidelines/components/drawer.md +961 -0
- package/guidelines/components/heading.md +505 -0
- package/guidelines/components/popover.md +1200 -0
- package/guidelines/components/progress.md +773 -0
- package/guidelines/components/radio-group.md +757 -0
- package/guidelines/components/select.md +1155 -0
- package/guidelines/components/skeleton.md +726 -0
- package/guidelines/components/tabs.md +834 -0
- package/guidelines/components/textarea.md +425 -0
- package/guidelines/components/toast.md +707 -0
- package/guidelines/components/tooltip.md +832 -0
- package/guidelines/overview-components.md +56 -8
- package/package.json +1 -1
|
@@ -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
|