@ngrr/ds 0.1.4 → 0.1.5
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/AGENTS.md +451 -0
- package/AI.md +1976 -0
- package/CLAUDE.md +451 -0
- package/dist/ds-nagarro.es.js +13369 -16845
- package/dist/ds-nagarro.umd.js +81 -81
- package/package.json +5 -2
package/AI.md
ADDED
|
@@ -0,0 +1,1976 @@
|
|
|
1
|
+
# DS-Nagarro — AI Agent Rules
|
|
2
|
+
|
|
3
|
+
> **Single source of truth for AI agents generating code for this design system.**
|
|
4
|
+
> Compiled from: `foundations.md`, `ds-guidelines.md`, all component docs in `docs/`, and `accessibility-audit-2026-02-28.md`.
|
|
5
|
+
> When in doubt, defer to the specific component doc file.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## How to use this file
|
|
10
|
+
|
|
11
|
+
1. **Before selecting a component**, check the [Cross-component patterns](#cross-component-patterns) section to determine the correct component from a decision tree.
|
|
12
|
+
2. **Before implementing a component**, read its section below for all props, states, tokens, ARIA requirements, and content rules.
|
|
13
|
+
3. **All code must comply** with [Accessibility requirements](#accessibility-requirements) and avoid everything in [What NOT to do](#what-not-to-do).
|
|
14
|
+
4. **All token references** must use CSS custom properties: `var(--token-name)`. Never hardcode values.
|
|
15
|
+
5. Token naming: Figma slash paths → hyphenated CSS vars. `background/interactive/cta/default` → `var(--background-interactive-cta-default)`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Token System Rules
|
|
20
|
+
|
|
21
|
+
These rules apply globally to every component.
|
|
22
|
+
|
|
23
|
+
### Three-tier model — always resolve through this chain
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Primitives (raw) → Semantic (intent) → Component (usage)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- **Use semantic tokens** in all component code. `var(--color-text-primary)` ✅ — `var(--primitive-neutral-900)` ❌
|
|
30
|
+
- **Component tokens** (`var(--color-surface-button-*)`): use these for component-specific overrides; they exist for a reason.
|
|
31
|
+
- **Never hardcode** hex codes, pixel values, or rgba. Everything comes from tokens.
|
|
32
|
+
|
|
33
|
+
### Token families and their CSS properties
|
|
34
|
+
|
|
35
|
+
| Token family | CSS property |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `--color-text-*` | `color` |
|
|
38
|
+
| `--background-*` | `background-color` |
|
|
39
|
+
| `--borders-*` | `border-color`, `outline-color` |
|
|
40
|
+
| `--color-surface-*` | `background-color` (component-level) |
|
|
41
|
+
| `--font-size-*`, `--font-weight-*`, `--font-line-height-*` | typography |
|
|
42
|
+
| `--space-*` | `gap`, `margin` (between elements) |
|
|
43
|
+
| `--inset-*` | `padding` (inside components) |
|
|
44
|
+
| `--radius-*` | `border-radius` |
|
|
45
|
+
| `--border-width-*` | `border-width` |
|
|
46
|
+
| `--effects-elevation-*` | `box-shadow` |
|
|
47
|
+
| `--transition-*` | `transition` |
|
|
48
|
+
|
|
49
|
+
### Space vs Inset — never mix
|
|
50
|
+
|
|
51
|
+
- `space/*` → gaps **between** elements (gap, margin)
|
|
52
|
+
- `inset/*` → padding **inside** a component
|
|
53
|
+
|
|
54
|
+
✅ `gap: var(--space-standard)` between items
|
|
55
|
+
✅ `padding: var(--inset-standard)` inside a button
|
|
56
|
+
❌ `gap: var(--inset-standard)` — wrong scale for context
|
|
57
|
+
|
|
58
|
+
### State name convention
|
|
59
|
+
|
|
60
|
+
Always use these exact identifiers. Never use `base`, `normal`, or `rest`.
|
|
61
|
+
|
|
62
|
+
| State | Suffix |
|
|
63
|
+
|---|---|
|
|
64
|
+
| Resting | `default` |
|
|
65
|
+
| Pointer over | `hover` |
|
|
66
|
+
| Mouse pressed | `pressed` |
|
|
67
|
+
| Keyboard focused | `focused` |
|
|
68
|
+
| Non-interactive | `disabled` |
|
|
69
|
+
| Active selection | `selected` (boolean prop, not state variant) |
|
|
70
|
+
| Error | `error` |
|
|
71
|
+
| Success | `success` |
|
|
72
|
+
|
|
73
|
+
### Size naming convention
|
|
74
|
+
|
|
75
|
+
| Figma name | Prop value |
|
|
76
|
+
|---|---|
|
|
77
|
+
| Small | `sm` |
|
|
78
|
+
| Medium | `md` |
|
|
79
|
+
| Large | `lg` |
|
|
80
|
+
| xLarge | `xl` |
|
|
81
|
+
| xxLarge | `2xl` |
|
|
82
|
+
|
|
83
|
+
`md` is always the default. When no size specified, generate with `md`.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Component Usage Rules
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### Button
|
|
92
|
+
|
|
93
|
+
**Applies to: `Button` (Main, Destructive, Toggle, Vertical)**
|
|
94
|
+
|
|
95
|
+
#### When to use
|
|
96
|
+
|
|
97
|
+
- **Main button**: default for actions. Not destructive, not icon-only, not vertical.
|
|
98
|
+
- **Destructive button**: irreversible or harmful actions (delete, discard). Same variants as Main but danger color.
|
|
99
|
+
- **Toggle button**: persistent on/off state in toolbars (Bold, Italic, view mode). NOT a one-off action trigger.
|
|
100
|
+
- **Vertical button**: icon + label stacked, in bottom toolbars only.
|
|
101
|
+
|
|
102
|
+
#### Do NOT use a button when
|
|
103
|
+
- Action navigates to a page → use a link
|
|
104
|
+
- Action toggles a setting → use Switcher
|
|
105
|
+
- Action selects from a set → use Segment control or Tab
|
|
106
|
+
|
|
107
|
+
#### Props and variants
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'plain';
|
|
111
|
+
type ButtonSize = 'sm' | 'md';
|
|
112
|
+
// plain is only available in sm
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Hierarchy rules
|
|
116
|
+
- **One Primary per view.** Never two Primary buttons side by side unless forced mutually exclusive choice.
|
|
117
|
+
- **Secondary**: supports Primary or equal-weight alternative.
|
|
118
|
+
- **Ghost**: functional/utility (filter, sort, copy). Not a "quieter Primary".
|
|
119
|
+
- **Plain**: dismissive, space-constrained, `sm` only. "Cancel", "Skip".
|
|
120
|
+
|
|
121
|
+
#### States
|
|
122
|
+
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled` · `Loading` · `Selected` *(Toggle/Vertical only)*
|
|
123
|
+
|
|
124
|
+
- `Selected` is a boolean prop. Use `aria-pressed="true/false"` for Toggle/Vertical.
|
|
125
|
+
- Do NOT use `aria-pressed` on Main or Destructive buttons.
|
|
126
|
+
- `Loading` → use `aria-busy="true"` on the button.
|
|
127
|
+
- `Disabled` → never rely on opacity alone. Do not disable Primary as a form validation strategy.
|
|
128
|
+
|
|
129
|
+
#### Placement
|
|
130
|
+
- Button group order left-to-right: Ghost/Plain → Secondary → **Primary** (most important is trailing).
|
|
131
|
+
- Destructive confirmation: Destructive right, "Cancel" left.
|
|
132
|
+
- Toggle buttons → toolbars only.
|
|
133
|
+
- Vertical buttons → bottom toolbars/action panels only.
|
|
134
|
+
|
|
135
|
+
#### ARIA
|
|
136
|
+
```tsx
|
|
137
|
+
<button type="button" disabled={disabled} aria-pressed={selected} aria-busy={loading} aria-label="...for icon-only">
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Content rules
|
|
141
|
+
- Start with a strong verb: "Save", "Delete", "Export"
|
|
142
|
+
- Specific: "Save changes" not "OK" or "Yes"
|
|
143
|
+
- 1–3 words; 5 maximum
|
|
144
|
+
- Sentence case: "Save changes" not "Save Changes"
|
|
145
|
+
- Destructive labels: name what is destroyed. "Delete account" not "Confirm"
|
|
146
|
+
- Loading labels: present-progressive. "Saving…"
|
|
147
|
+
- Toggle `aria-label`: describes the action, not the state. "Bold" not "Bold is active"
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### Avatar / AvatarGroup
|
|
152
|
+
|
|
153
|
+
**Applies to: `Avatar`, `AvatarGroup`**
|
|
154
|
+
|
|
155
|
+
#### When to use
|
|
156
|
+
- Represents a person or organization where visual identity aids recognition.
|
|
157
|
+
- Do NOT use for generic system processes or when no identity info is available.
|
|
158
|
+
|
|
159
|
+
#### Variants
|
|
160
|
+
- `Profile`: individual person
|
|
161
|
+
- `Organization`: company/team
|
|
162
|
+
- Never mix Profile and Organization in the same list without clear reason.
|
|
163
|
+
|
|
164
|
+
#### Fill priority (always use highest fidelity available)
|
|
165
|
+
1. `Picture` — real photo/logo
|
|
166
|
+
2. `Initials` — derived from name
|
|
167
|
+
3. `Icon` — anonymous fallback
|
|
168
|
+
|
|
169
|
+
Never show Icon when Initials are derivable.
|
|
170
|
+
|
|
171
|
+
#### Sizes (Avatar: 7-step scale)
|
|
172
|
+
`xxs` · `xs` · `sm` · `md` (default) · `lg` · `xl` · `xxl`
|
|
173
|
+
|
|
174
|
+
Use consistent sizes within a list. Never mix sizes in a collection.
|
|
175
|
+
|
|
176
|
+
#### AvatarGroup
|
|
177
|
+
- Sizes: `xxs`, `xs`, `sm` only
|
|
178
|
+
- Show "+" overflow indicator when list exceeds display limit (typically 3–5)
|
|
179
|
+
- Overflow indicator must include full list in `aria-label`
|
|
180
|
+
- Do not use for >20 members without a "view all" affordance
|
|
181
|
+
|
|
182
|
+
#### ARIA
|
|
183
|
+
```tsx
|
|
184
|
+
// Individual: meaningful avatar
|
|
185
|
+
<img alt="Ana Costa" aria-label="Ana Costa" />
|
|
186
|
+
|
|
187
|
+
// Icon fill, no identity
|
|
188
|
+
<span aria-hidden="true" />
|
|
189
|
+
|
|
190
|
+
// AvatarGroup
|
|
191
|
+
<div aria-label="Assigned to: Ana Costa, João Silva, and 2 others">
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### Content rules — Initials
|
|
195
|
+
- Person: first + last initial. "Ana Costa" → "AC"
|
|
196
|
+
- Single name: first two letters. "Cher" → "CH"
|
|
197
|
+
- Organization: first two letters or first letter of each word (max 2). "Design Studio" → "DS"
|
|
198
|
+
- Always uppercase. Max 2 characters.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
### Badge
|
|
203
|
+
|
|
204
|
+
**Applies to: `Badge`**
|
|
205
|
+
|
|
206
|
+
#### When to use
|
|
207
|
+
- Attach to another element to surface a count or status signal.
|
|
208
|
+
- Notification count on a navigation icon, pending actions, critical status.
|
|
209
|
+
|
|
210
|
+
#### Do NOT use when
|
|
211
|
+
- Label is text/category → use Tag
|
|
212
|
+
- User can select/dismiss it → use Chip
|
|
213
|
+
- Requires user response → use Toast or alert banner
|
|
214
|
+
- Count is zero → **hide the badge**
|
|
215
|
+
|
|
216
|
+
#### Variants
|
|
217
|
+
| Variant | Use |
|
|
218
|
+
|---|---|
|
|
219
|
+
| `Highlight soft` | Low urgency — new content, informational count |
|
|
220
|
+
| `Highlight strong` | Moderate urgency — pending actions, unread items |
|
|
221
|
+
| `Informative` | Neutral — generic counts |
|
|
222
|
+
| `Critical` | Urgent — errors, failures. **Use sparingly.** |
|
|
223
|
+
|
|
224
|
+
#### Sizes
|
|
225
|
+
- `sm`: default — icons, avatars, compact items
|
|
226
|
+
- `lg`: larger host elements
|
|
227
|
+
- `Dot`: presence-only signal. `Highlight strong` and `Critical` only.
|
|
228
|
+
|
|
229
|
+
#### Count display rules
|
|
230
|
+
- ≤99: show exact count
|
|
231
|
+
- >99: show "99+"
|
|
232
|
+
- 0: **remove the badge entirely**
|
|
233
|
+
|
|
234
|
+
#### ARIA
|
|
235
|
+
```tsx
|
|
236
|
+
// Host element's accessible name must include the count
|
|
237
|
+
<button aria-label="Messages, 3 unread" />
|
|
238
|
+
// Dynamic count changes
|
|
239
|
+
<div aria-live="polite" /> // only for meaningful threshold transitions
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### Tag
|
|
245
|
+
|
|
246
|
+
**Applies to: `Tag`**
|
|
247
|
+
|
|
248
|
+
#### When to use
|
|
249
|
+
- Non-interactive categorical label or status on content items.
|
|
250
|
+
- Status: "Active", "Archived", "Pending review"
|
|
251
|
+
- Category: "Design", "Bug", "High"
|
|
252
|
+
|
|
253
|
+
#### Do NOT use when
|
|
254
|
+
- User can interact with it → use Chip
|
|
255
|
+
- It's a numeric count → use Badge
|
|
256
|
+
- Urgent system condition → use Toast
|
|
257
|
+
|
|
258
|
+
#### Variants
|
|
259
|
+
| Variant | Semantic meaning |
|
|
260
|
+
|---|---|
|
|
261
|
+
| `Highlight` | Notable, not urgent |
|
|
262
|
+
| `Warning` | Caution — needs awareness soon |
|
|
263
|
+
| `Error` | Something is wrong or blocked |
|
|
264
|
+
| `Success` | Completed, approved, healthy |
|
|
265
|
+
| `Neutral` | Purely categorical, no status weight |
|
|
266
|
+
|
|
267
|
+
Use the variant matching semantic meaning, not preferred color.
|
|
268
|
+
|
|
269
|
+
#### Sizes
|
|
270
|
+
- `md`: default
|
|
271
|
+
- `sm`: dense layouts, multiple tags per item
|
|
272
|
+
|
|
273
|
+
Use consistent sizes within a list. Never mix on the same row.
|
|
274
|
+
|
|
275
|
+
#### Tags have NO interaction states. Not clickable. Never.
|
|
276
|
+
|
|
277
|
+
#### Placement
|
|
278
|
+
- Show max 3 tags per item.
|
|
279
|
+
- Order: severity first (Error → Warning → Success), then category.
|
|
280
|
+
- Do not wrap — truncate and show full value in tooltip if space is insufficient.
|
|
281
|
+
|
|
282
|
+
#### ARIA
|
|
283
|
+
```tsx
|
|
284
|
+
// Include tag content in accessible name of parent
|
|
285
|
+
<li aria-describedby="tag-status">
|
|
286
|
+
<span id="tag-status" role="status">Error</span>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
### Chip / ChipGroup
|
|
292
|
+
|
|
293
|
+
**Applies to: `Chip`, `ChipGroup`**
|
|
294
|
+
|
|
295
|
+
#### When to use
|
|
296
|
+
- User needs to select, activate, or dismiss a discrete value.
|
|
297
|
+
- Filter controls, multi-value input, toggleable options.
|
|
298
|
+
|
|
299
|
+
#### Do NOT use when
|
|
300
|
+
- Purely informational → use Tag
|
|
301
|
+
- Mutually exclusive form selection → use Radio
|
|
302
|
+
- Multi-option form selection → use Checkbox
|
|
303
|
+
- Settings toggle → use Switcher
|
|
304
|
+
|
|
305
|
+
#### Sizes
|
|
306
|
+
- `md`: default
|
|
307
|
+
- `sm`: compact layouts
|
|
308
|
+
|
|
309
|
+
Never mix `md` and `sm` in the same chip group.
|
|
310
|
+
|
|
311
|
+
#### States
|
|
312
|
+
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled` · `Selected` (boolean)
|
|
313
|
+
|
|
314
|
+
`Selected` combines freely with `Hover`, `Pressed`, `Focused`.
|
|
315
|
+
|
|
316
|
+
#### Keyboard interaction
|
|
317
|
+
- `Space` or `Enter` → toggle
|
|
318
|
+
- `Tab` → move between chips
|
|
319
|
+
|
|
320
|
+
#### ARIA
|
|
321
|
+
```tsx
|
|
322
|
+
// Multi-select filter group
|
|
323
|
+
<div role="group" aria-label="Filter by status">
|
|
324
|
+
<button role="checkbox" aria-checked="true">Active</button>
|
|
325
|
+
<button role="checkbox" aria-checked="false">Closed</button>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
// Single-select group
|
|
329
|
+
<div role="radiogroup" aria-label="Filter by status">
|
|
330
|
+
<button role="radio" aria-checked="true">Active</button>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
// Dismiss button on chip
|
|
334
|
+
<button aria-label="Remove Active" />
|
|
335
|
+
|
|
336
|
+
// Disabled chip
|
|
337
|
+
aria-disabled="true" // keep in reading order
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
### Separator
|
|
343
|
+
|
|
344
|
+
**Applies to: `Separator`**
|
|
345
|
+
|
|
346
|
+
#### When to use
|
|
347
|
+
- Visual divider between content sections that are related but distinct.
|
|
348
|
+
|
|
349
|
+
#### Do NOT use when
|
|
350
|
+
- Adequate spacing already separates sections
|
|
351
|
+
- Sections are already separated by color/card borders
|
|
352
|
+
- Purpose is purely decorative
|
|
353
|
+
|
|
354
|
+
#### Directions
|
|
355
|
+
- `Horizontal`: stacked content, spans full container width
|
|
356
|
+
- `Vertical`: side-by-side content, spans full container height
|
|
357
|
+
|
|
358
|
+
#### ARIA
|
|
359
|
+
```tsx
|
|
360
|
+
// Structural separator
|
|
361
|
+
<hr /> // or <div role="separator" />
|
|
362
|
+
|
|
363
|
+
// Decorative only
|
|
364
|
+
<div aria-hidden="true" />
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
### Switcher
|
|
370
|
+
|
|
371
|
+
**Applies to: `Switcher`**
|
|
372
|
+
|
|
373
|
+
#### When to use
|
|
374
|
+
- Single binary setting, **effect is immediate** (no confirmation step).
|
|
375
|
+
- Enabling features, notification settings, preferences.
|
|
376
|
+
|
|
377
|
+
#### Do NOT use when
|
|
378
|
+
- More than 2 states → use Radio or Select
|
|
379
|
+
- Multiple independent options → use Checkbox
|
|
380
|
+
- Requires confirmation before taking effect → use Checkbox
|
|
381
|
+
- Compact toolbar → use Toggle button
|
|
382
|
+
- Choosing between UI views/modes → use Segment control or Tabs
|
|
383
|
+
|
|
384
|
+
#### Decision: Switcher vs Checkbox vs Toggle button
|
|
385
|
+
| Component | Effect | Context |
|
|
386
|
+
|---|---|---|
|
|
387
|
+
| **Switcher** | Immediate | Settings with visible label |
|
|
388
|
+
| **Checkbox** | Staged (on submit) | Forms |
|
|
389
|
+
| **Toggle button** | Immediate | Icon-only toolbar |
|
|
390
|
+
|
|
391
|
+
#### Sizes
|
|
392
|
+
- `md`: default
|
|
393
|
+
- `sm`: dense layouts
|
|
394
|
+
|
|
395
|
+
Never mix sizes within a settings group.
|
|
396
|
+
|
|
397
|
+
#### States
|
|
398
|
+
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled`
|
|
399
|
+
|
|
400
|
+
`Selected` and `Disabled` are independent booleans.
|
|
401
|
+
|
|
402
|
+
#### Rules
|
|
403
|
+
- Always pair with a visible label. **Never use a switcher without a label.**
|
|
404
|
+
- When `Selected=True` + `Disabled=True`: always explain why it's locked.
|
|
405
|
+
- If toggle has latency, show loading indicator alongside — don't leave in ambiguous state.
|
|
406
|
+
|
|
407
|
+
#### Keyboard
|
|
408
|
+
- `Space` → toggle
|
|
409
|
+
|
|
410
|
+
#### ARIA
|
|
411
|
+
```tsx
|
|
412
|
+
<button role="switch" aria-checked={isOn} aria-labelledby="label-id" />
|
|
413
|
+
// Disabled
|
|
414
|
+
aria-disabled="true" // keep in reading order
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
### Checkbox
|
|
420
|
+
|
|
421
|
+
**Applies to: `Checkbox`**
|
|
422
|
+
|
|
423
|
+
#### When to use
|
|
424
|
+
- User selects any number of independent options (zero to all).
|
|
425
|
+
- Selection takes effect only after **explicit confirmation** (submit button).
|
|
426
|
+
|
|
427
|
+
#### Do NOT use when
|
|
428
|
+
- Only one can be selected → use Radio
|
|
429
|
+
- Effect is immediate → use Switcher
|
|
430
|
+
- Compact token-like pattern → use Chip
|
|
431
|
+
- Single toggle for a setting → use Switcher
|
|
432
|
+
|
|
433
|
+
#### Sizes
|
|
434
|
+
- `md`: default
|
|
435
|
+
- `sm`: dense layouts
|
|
436
|
+
|
|
437
|
+
Never mix sizes in a single form group.
|
|
438
|
+
|
|
439
|
+
#### States
|
|
440
|
+
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled`
|
|
441
|
+
|
|
442
|
+
**Indeterminate state**: when a parent checkbox controls sub-items with partial selection.
|
|
443
|
+
|
|
444
|
+
#### Interaction
|
|
445
|
+
- Click on checkbox **or its label** toggles selection (full row is hit target).
|
|
446
|
+
- State changes are staged — not immediate.
|
|
447
|
+
|
|
448
|
+
#### ARIA
|
|
449
|
+
```tsx
|
|
450
|
+
<fieldset>
|
|
451
|
+
<legend>Notification preferences</legend>
|
|
452
|
+
<label>
|
|
453
|
+
<input type="checkbox" aria-checked="true" /> Send weekly summary
|
|
454
|
+
</label>
|
|
455
|
+
</fieldset>
|
|
456
|
+
|
|
457
|
+
// Indeterminate
|
|
458
|
+
aria-checked="mixed"
|
|
459
|
+
|
|
460
|
+
// Disabled — keep in reading order
|
|
461
|
+
aria-disabled="true"
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
### Radio
|
|
467
|
+
|
|
468
|
+
**Applies to: `Radio`**
|
|
469
|
+
|
|
470
|
+
#### When to use
|
|
471
|
+
- Exactly one option from a mutually exclusive set.
|
|
472
|
+
- All options visible simultaneously (2–6 items).
|
|
473
|
+
- Effect takes place after **explicit confirmation**.
|
|
474
|
+
|
|
475
|
+
#### Do NOT use when
|
|
476
|
+
- Multiple can be selected → use Checkbox
|
|
477
|
+
- More than 6–7 options → use Select
|
|
478
|
+
- Immediate effect → use Switcher or Segment control
|
|
479
|
+
- Single on/off option → use Checkbox or Switcher
|
|
480
|
+
|
|
481
|
+
#### Sizes
|
|
482
|
+
- `md`: default
|
|
483
|
+
- `sm`: dense layouts
|
|
484
|
+
|
|
485
|
+
Never mix sizes within a radio group.
|
|
486
|
+
|
|
487
|
+
#### States
|
|
488
|
+
`Default` · `Hover` · `Pressed` · `Focused` · `Disabled`
|
|
489
|
+
|
|
490
|
+
No indeterminate state for Radio. A group always has 0–1 selected.
|
|
491
|
+
|
|
492
|
+
#### Interaction
|
|
493
|
+
- Click radio **or its label** to select (full row is hit target).
|
|
494
|
+
- Selecting one deselects all others in the group.
|
|
495
|
+
- Keyboard: `Arrow keys` move selection within group. `Tab` exits group.
|
|
496
|
+
|
|
497
|
+
#### ARIA
|
|
498
|
+
```tsx
|
|
499
|
+
<div role="radiogroup" aria-labelledby="group-label">
|
|
500
|
+
<label>
|
|
501
|
+
<input type="radio" aria-checked="true" /> Option A
|
|
502
|
+
</label>
|
|
503
|
+
</div>
|
|
504
|
+
// Arrow key navigation must move both focus and selection together
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
### Tab / CustomViewTab
|
|
510
|
+
|
|
511
|
+
**Applies to: `Tab` (Navigation, Custom View)**
|
|
512
|
+
|
|
513
|
+
#### When to use
|
|
514
|
+
- Parallel, mutually exclusive content sections on the same page.
|
|
515
|
+
- Each section is substantial enough to be its own named panel.
|
|
516
|
+
- 2–7 tabs.
|
|
517
|
+
|
|
518
|
+
#### Do NOT use when
|
|
519
|
+
- Sections are sequential steps → use stepper
|
|
520
|
+
- Fewer than 2 tabs
|
|
521
|
+
- More than 6–7 tabs → use sidebar nav or Select
|
|
522
|
+
- Goal is filtering same content → use Chip
|
|
523
|
+
|
|
524
|
+
#### Tab vs Segment control
|
|
525
|
+
| | Segment control | Tab |
|
|
526
|
+
|---|---|---|
|
|
527
|
+
| What changes | How content is rendered | The content itself |
|
|
528
|
+
| Architecture | Display preference | Part of information architecture |
|
|
529
|
+
| URL typically | Does not update | Updates (sub-routes) |
|
|
530
|
+
|
|
531
|
+
#### Rules
|
|
532
|
+
- **One tab must always be selected.** No valid unselected state.
|
|
533
|
+
- All tabs in a group must be the same type.
|
|
534
|
+
- Tab labels: nouns only. No verbs. "Overview" not "View overview".
|
|
535
|
+
- Disabled tabs: explain via tooltip. Remove if always disabled for all users.
|
|
536
|
+
- Content panel must be directly below/adjacent to tabs — never disconnected.
|
|
537
|
+
- Never nest tabs within tabs.
|
|
538
|
+
|
|
539
|
+
#### Keyboard
|
|
540
|
+
- `Tab` → focus tab row
|
|
541
|
+
- `←` `→` → move between tabs
|
|
542
|
+
- `Enter` / `Space` → select focused tab
|
|
543
|
+
- `Tab` → move into content panel
|
|
544
|
+
|
|
545
|
+
#### ARIA
|
|
546
|
+
```tsx
|
|
547
|
+
<div role="tablist">
|
|
548
|
+
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1" tabIndex={0}>Overview</button>
|
|
549
|
+
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabIndex={-1}>Activity</button>
|
|
550
|
+
</div>
|
|
551
|
+
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">...</div>
|
|
552
|
+
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>...</div>
|
|
553
|
+
// Only selected tab has tabIndex=0. All others tabIndex=-1.
|
|
554
|
+
// Inactive panels: hidden or aria-hidden="true"
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
### SegmentControl
|
|
560
|
+
|
|
561
|
+
**Applies to: `SegmentControl`, `SegmentControlSelector`**
|
|
562
|
+
|
|
563
|
+
#### When to use
|
|
564
|
+
- Switch between 2–5 mutually exclusive views or modes.
|
|
565
|
+
- Effect is **immediate** — no content panels, no confirmation.
|
|
566
|
+
- "List", "Grid", "Map" / "Day", "Week", "Month"
|
|
567
|
+
|
|
568
|
+
#### Do NOT use when
|
|
569
|
+
- More than 5 options → use Select or Tabs
|
|
570
|
+
- Options have distinct content panels → use Tabs
|
|
571
|
+
- Requires confirmation → use Radio
|
|
572
|
+
- Binary setting → use Switcher
|
|
573
|
+
- Stackable filters → use Chips
|
|
574
|
+
|
|
575
|
+
#### Rules
|
|
576
|
+
- **One selector always active.** No valid unselected state.
|
|
577
|
+
- All selectors must be equal width.
|
|
578
|
+
- Max 5 options.
|
|
579
|
+
- Never use as navigation.
|
|
580
|
+
|
|
581
|
+
#### ARIA
|
|
582
|
+
```tsx
|
|
583
|
+
<div role="group" aria-label="View mode">
|
|
584
|
+
<button role="radio" aria-checked="true">List</button>
|
|
585
|
+
<button role="radio" aria-checked="false">Grid</button>
|
|
586
|
+
</div>
|
|
587
|
+
// Arrow key navigation moves focus and selection together
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
### Breadcrumbs / BreadcrumbItem
|
|
593
|
+
|
|
594
|
+
**Applies to: `Breadcrumbs`, `BreadcrumbItem`**
|
|
595
|
+
|
|
596
|
+
#### When to use
|
|
597
|
+
- Clear hierarchy of at least 3 levels.
|
|
598
|
+
- Users navigate between levels frequently.
|
|
599
|
+
|
|
600
|
+
#### Do NOT use when
|
|
601
|
+
- Hierarchy is flat (≤2 levels)
|
|
602
|
+
- Single-level navigation pattern
|
|
603
|
+
- User arrived via deep link and path is not meaningful
|
|
604
|
+
|
|
605
|
+
#### Item types
|
|
606
|
+
| Type | Description |
|
|
607
|
+
|---|---|
|
|
608
|
+
| `Previous` | Ancestor level — interactive link |
|
|
609
|
+
| `Current` | Current location — not a link, not interactive |
|
|
610
|
+
| `Ellipsis` | Collapsed middle levels — expandable on click |
|
|
611
|
+
|
|
612
|
+
- **Last item is always `Current`.** Must not be a link.
|
|
613
|
+
- Never truncate `Current` item.
|
|
614
|
+
- When trail overflows: always keep root (leftmost) and current (rightmost).
|
|
615
|
+
|
|
616
|
+
#### ARIA
|
|
617
|
+
```tsx
|
|
618
|
+
<nav aria-label="Breadcrumb">
|
|
619
|
+
<ol>
|
|
620
|
+
<li><a href="/home">Home</a></li>
|
|
621
|
+
<li><a href="/projects">Projects</a></li>
|
|
622
|
+
<li><span aria-current="page">Q4 Campaign</span></li>
|
|
623
|
+
</ol>
|
|
624
|
+
</nav>
|
|
625
|
+
// Ellipsis
|
|
626
|
+
<button aria-label="Show more breadcrumbs">…</button>
|
|
627
|
+
// Separators — decorative, must not be read aloud
|
|
628
|
+
<span aria-hidden="true">/</span>
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
### Pagination / PageSelector
|
|
634
|
+
|
|
635
|
+
**Applies to: `Pagination`, `PageSelector`**
|
|
636
|
+
|
|
637
|
+
#### When to use
|
|
638
|
+
- Large dataset divided into pages where user benefits from explicit navigation.
|
|
639
|
+
- Data tables, search results, record lists.
|
|
640
|
+
|
|
641
|
+
#### Do NOT use when
|
|
642
|
+
- Dataset fits in one view → show everything
|
|
643
|
+
- Primary interaction is continuous browsing → use infinite scroll
|
|
644
|
+
- Within a compact panel → use load-more button
|
|
645
|
+
|
|
646
|
+
#### Rules
|
|
647
|
+
- Always show current page as `Selected`.
|
|
648
|
+
- Always show first and last page numbers.
|
|
649
|
+
- Collapse middle ranges with ellipsis; keep current page ± 1–2 neighbors.
|
|
650
|
+
- Previous/Next arrows: **disable** at boundaries, never hide.
|
|
651
|
+
- Show total record count: "Showing 41–60 of 243 results".
|
|
652
|
+
- After page navigation: move focus to top of updated content area.
|
|
653
|
+
|
|
654
|
+
#### ARIA
|
|
655
|
+
```tsx
|
|
656
|
+
<nav aria-label="Pagination">
|
|
657
|
+
<button aria-label="Go to previous page" aria-disabled="true">←</button>
|
|
658
|
+
<button aria-current="page">3</button>
|
|
659
|
+
<button>4</button>
|
|
660
|
+
<button aria-label="More pages">…</button>
|
|
661
|
+
<button aria-label="Go to next page">→</button>
|
|
662
|
+
</nav>
|
|
663
|
+
// Page change: use aria-live region to announce update
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
### Toast (Inline, Rich)
|
|
669
|
+
|
|
670
|
+
**Applies to: `Toast`**
|
|
671
|
+
|
|
672
|
+
#### When to use
|
|
673
|
+
- Confirm a completed action or surface a system event without interrupting the current task.
|
|
674
|
+
- Non-blocking. Temporary.
|
|
675
|
+
|
|
676
|
+
#### Do NOT use when
|
|
677
|
+
- Critical, requires action before continuing → use modal or alert banner
|
|
678
|
+
- Related to a specific form field → use inline validation
|
|
679
|
+
- Complex, multiple actions needed → use Rich toast or reconsider modal
|
|
680
|
+
- Unsolicited marketing/promotional content → never appropriate
|
|
681
|
+
|
|
682
|
+
#### Types
|
|
683
|
+
- **Inline toast**: single message, brief confirmation.
|
|
684
|
+
- **Rich toast**: title + body + optional action. Use only when additional space is genuinely needed.
|
|
685
|
+
|
|
686
|
+
#### Severity
|
|
687
|
+
| Severity | Use |
|
|
688
|
+
|---|---|
|
|
689
|
+
| `Default` | Neutral confirmations, background tasks |
|
|
690
|
+
| `Success` | Most common — action completed as expected |
|
|
691
|
+
| `Warning` | Completed with caveat, system condition |
|
|
692
|
+
| `Error` | Genuine failures only — **persists until dismissed** |
|
|
693
|
+
|
|
694
|
+
#### Auto-dismiss durations
|
|
695
|
+
- `Default` / `Success`: 4–5 seconds
|
|
696
|
+
- `Warning`: 6–8 seconds
|
|
697
|
+
- `Error`: persistent or 8–10 seconds minimum
|
|
698
|
+
- Rich toast with action: persist until user acts or dismisses
|
|
699
|
+
|
|
700
|
+
Always provide a manual dismiss (×) button.
|
|
701
|
+
Pause auto-dismiss on hover (desktop).
|
|
702
|
+
|
|
703
|
+
#### ARIA
|
|
704
|
+
```tsx
|
|
705
|
+
// Success, warning, default
|
|
706
|
+
<div aria-live="polite" role="status">Changes saved.</div>
|
|
707
|
+
|
|
708
|
+
// Error
|
|
709
|
+
<div aria-live="assertive" role="alert">Couldn't save changes.</div>
|
|
710
|
+
|
|
711
|
+
// Dismiss button
|
|
712
|
+
<button aria-label="Dismiss notification">×</button>
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
---
|
|
716
|
+
|
|
717
|
+
### Tooltip
|
|
718
|
+
|
|
719
|
+
**Applies to: `Tooltip`**
|
|
720
|
+
|
|
721
|
+
#### When to use
|
|
722
|
+
- Brief supplementary context — hover or keyboard focus only.
|
|
723
|
+
- Label icon-only buttons, reveal truncated text, show keyboard shortcuts.
|
|
724
|
+
|
|
725
|
+
#### Do NOT use when
|
|
726
|
+
- Critical info user must see → place inline
|
|
727
|
+
- Content longer than one sentence → use Popover
|
|
728
|
+
- Content has interactive elements → use Popover
|
|
729
|
+
- Trigger is disabled → use visible text explanation instead
|
|
730
|
+
- Touch device as primary platform → ensure info is available another way
|
|
731
|
+
|
|
732
|
+
#### Rules
|
|
733
|
+
- Plain text only. No icons, buttons, or links.
|
|
734
|
+
- One sentence maximum.
|
|
735
|
+
- Appears on hover (100–200ms delay) and keyboard focus. Never on click.
|
|
736
|
+
- Dismiss: cursor leaves, focus moves, or `Escape`.
|
|
737
|
+
|
|
738
|
+
#### ARIA
|
|
739
|
+
```tsx
|
|
740
|
+
<button aria-label="Export as CSV" aria-describedby="tooltip-export">Export</button>
|
|
741
|
+
<div role="tooltip" id="tooltip-export">Export table as CSV</div>
|
|
742
|
+
// Tooltip text and aria-label must match for icon-only buttons
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
### Popover (Container, Simple Menu, Complex Menu)
|
|
748
|
+
|
|
749
|
+
**Applies to: `Popover`**
|
|
750
|
+
|
|
751
|
+
#### When to use
|
|
752
|
+
- Contextual rich content anchored to an element. User clicks to open.
|
|
753
|
+
- Definitions, action menus, date picker triggers, quick-edit panels.
|
|
754
|
+
|
|
755
|
+
#### Do NOT use when
|
|
756
|
+
- Single short sentence, no interaction → use Tooltip
|
|
757
|
+
- Demands full attention, blocks UI → use Modal
|
|
758
|
+
- Extended task the user interacts with over time → use Drawer
|
|
759
|
+
- Navigation menu → use dedicated nav component
|
|
760
|
+
|
|
761
|
+
#### Types
|
|
762
|
+
- **Container**: free-form rich content
|
|
763
|
+
- **Simple menu**: flat list, 2–6 items, no grouping
|
|
764
|
+
- **Complex menu**: grouped, with section headings, scrollable
|
|
765
|
+
|
|
766
|
+
#### Rules
|
|
767
|
+
- Opens on **click/tap** only. Never on hover.
|
|
768
|
+
- Dismissal: click outside, `Escape`, trigger toggle, or completing action.
|
|
769
|
+
- Do not nest popovers inside other popovers.
|
|
770
|
+
- For menus: selecting an item closes popover automatically.
|
|
771
|
+
- For container with unsaved form input: do NOT auto-close on outside click.
|
|
772
|
+
|
|
773
|
+
#### Positioning contract (shared `PopoverWrapper`)
|
|
774
|
+
- Default placement: below trigger
|
|
775
|
+
- Gap: `var(--space-tiny)`
|
|
776
|
+
- Flip: only when not enough space below
|
|
777
|
+
- Viewport safe margin: `var(--space-medium)` on all edges
|
|
778
|
+
- Width: match trigger width, minimum 192px
|
|
779
|
+
- Never re-implement per-component popover math — reuse `PopoverWrapper`
|
|
780
|
+
|
|
781
|
+
#### ARIA
|
|
782
|
+
```tsx
|
|
783
|
+
// Trigger
|
|
784
|
+
<button aria-haspopup="true" aria-expanded={isOpen}>Options</button>
|
|
785
|
+
|
|
786
|
+
// Menu type
|
|
787
|
+
<div role="menu">
|
|
788
|
+
<button role="menuitem">Edit</button>
|
|
789
|
+
<button role="menuitem">Delete</button>
|
|
790
|
+
</div>
|
|
791
|
+
|
|
792
|
+
// Container type
|
|
793
|
+
<div role="dialog" aria-label="Filter options">...</div>
|
|
794
|
+
|
|
795
|
+
// On open: focus moves INTO popover (first interactive element)
|
|
796
|
+
// On close: focus returns to trigger element
|
|
797
|
+
// Escape: closes and returns focus
|
|
798
|
+
// Arrow keys: navigate between menuitem elements
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
### Modal
|
|
804
|
+
|
|
805
|
+
**Applies to: `Modal`**
|
|
806
|
+
|
|
807
|
+
#### When to use
|
|
808
|
+
- User must complete a task or make a decision before continuing.
|
|
809
|
+
- Confirmations, compact forms, critical warnings, media previews.
|
|
810
|
+
|
|
811
|
+
#### Do NOT use when
|
|
812
|
+
- Informational only → use Toast or inline message
|
|
813
|
+
- Long, multi-step, complex task → use full page or Drawer
|
|
814
|
+
- Contextually anchored to an element → use Popover
|
|
815
|
+
- User needs to reference background content → use Drawer
|
|
816
|
+
|
|
817
|
+
#### Rules
|
|
818
|
+
- Every modal must have a **title**.
|
|
819
|
+
- Every modal must have an **explicit dismiss** (× in header + cancel in footer).
|
|
820
|
+
- **One primary action only.** Two equal actions → Secondary + Secondary, not two Primary.
|
|
821
|
+
- Never open a modal from within a modal.
|
|
822
|
+
- Never auto-open on page load.
|
|
823
|
+
- `Escape` must close unless accidental dismissal would cause irreversible data loss.
|
|
824
|
+
- Background page scroll must be disabled while modal is open.
|
|
825
|
+
|
|
826
|
+
#### Interaction
|
|
827
|
+
- Opening: explicit user action only.
|
|
828
|
+
- Focus moves INTO modal on open (first interactive element or container).
|
|
829
|
+
- Focus is **trapped** within modal while open.
|
|
830
|
+
- Focus returns to triggering element on close.
|
|
831
|
+
|
|
832
|
+
#### ARIA
|
|
833
|
+
```tsx
|
|
834
|
+
<div role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-desc">
|
|
835
|
+
<h2 id="modal-title">Delete project</h2>
|
|
836
|
+
<p id="modal-desc">This will permanently delete the project and all its data. This cannot be undone.</p>
|
|
837
|
+
<div aria-hidden="true" class="overlay" /> {/* backdrop is aria-hidden */}
|
|
838
|
+
</div>
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
---
|
|
842
|
+
|
|
843
|
+
### Input — shared anatomy
|
|
844
|
+
|
|
845
|
+
**Applies to: all input components**
|
|
846
|
+
|
|
847
|
+
#### Anatomy
|
|
848
|
+
```
|
|
849
|
+
[Label] ← always required (visible or aria-label)
|
|
850
|
+
[Control] ← must have visible boundary
|
|
851
|
+
[Helper text] ← optional; replace with real content or hide — never leave placeholder text
|
|
852
|
+
[Error message] ← replaces helper text in error state
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
#### States
|
|
856
|
+
`Empty` · `Hover` · `Active` · `Filled` · `Focused` · `Error` · `Disabled`
|
|
857
|
+
|
|
858
|
+
- `Empty` ≠ `Filled`: never conflate. `Empty` has no value, `Filled` always does.
|
|
859
|
+
- `Active` ≠ `Focused`: Active = typing (cursor present). Focused = keyboard focus without typing.
|
|
860
|
+
- `Disabled`: use native `disabled` attribute or `aria-disabled="true"`. Not tab-focusable.
|
|
861
|
+
|
|
862
|
+
#### Validation timing
|
|
863
|
+
- **Default**: validate on **blur** (leaving the field).
|
|
864
|
+
- **Real-time** only for: password strength, character count limits, search/filter fields, OTP.
|
|
865
|
+
- On form submit: show **all errors at once**, scroll to and focus the first errored field.
|
|
866
|
+
- Once shown, re-validate in real time as user corrects. Remove error when value becomes valid.
|
|
867
|
+
- Never hide error on blur alone — only when value is actually valid.
|
|
868
|
+
|
|
869
|
+
#### Label rules
|
|
870
|
+
- Required for all inputs.
|
|
871
|
+
- Visible label above control is default and preferred.
|
|
872
|
+
- Left-of-control only in Horizontal input layouts.
|
|
873
|
+
- Never use placeholder text as label substitute.
|
|
874
|
+
- Label describes what the field contains: "Email address" not "Enter your email address".
|
|
875
|
+
- Every field is mandatory by default. Only mark optional fields with an "Optional" label in `var(--text-tertiary)` placed inline after the label text. Never use asterisks.
|
|
876
|
+
- Add `aria-required="true"` on all mandatory inputs regardless of visual treatment.
|
|
877
|
+
|
|
878
|
+
#### ARIA — all inputs
|
|
879
|
+
```tsx
|
|
880
|
+
<label htmlFor="email">Email address</label>
|
|
881
|
+
<input
|
|
882
|
+
id="email"
|
|
883
|
+
aria-describedby="email-helper email-error"
|
|
884
|
+
aria-invalid={hasError}
|
|
885
|
+
aria-required={required}
|
|
886
|
+
disabled={disabled}
|
|
887
|
+
/>
|
|
888
|
+
<p id="email-helper">We'll only use this to send receipts.</p>
|
|
889
|
+
<p id="email-error" aria-live="polite">Email address is invalid.</p>
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
---
|
|
893
|
+
|
|
894
|
+
### TextInput
|
|
895
|
+
|
|
896
|
+
**Applies to: `TextInput`**
|
|
897
|
+
|
|
898
|
+
Single-line free-form text. Use when no more specific input type applies.
|
|
899
|
+
|
|
900
|
+
Do NOT use for multi-line → TextArea | numbers → NumberInput | passwords → PasswordInput | phone → PhoneInput | search → SearchInput | predefined list → Select.
|
|
901
|
+
|
|
902
|
+
---
|
|
903
|
+
|
|
904
|
+
### TextArea
|
|
905
|
+
|
|
906
|
+
**Applies to: `TextArea`**
|
|
907
|
+
|
|
908
|
+
Multi-line free-form text.
|
|
909
|
+
|
|
910
|
+
- Minimum height: 3 lines
|
|
911
|
+
- Allow vertical resize (desktop)
|
|
912
|
+
- Show character count when limit exists: "120 characters remaining" not "380/500"
|
|
913
|
+
|
|
914
|
+
---
|
|
915
|
+
|
|
916
|
+
### SearchInput
|
|
917
|
+
|
|
918
|
+
**Applies to: `SearchInput`**
|
|
919
|
+
|
|
920
|
+
Search and filter interactions.
|
|
921
|
+
|
|
922
|
+
- **Bordered**: standard, full border
|
|
923
|
+
- **Borderless**: inline in toolbars/headers
|
|
924
|
+
- Always include clear (×) button when field has value.
|
|
925
|
+
- `Error` state is for system errors (service unavailable) — NOT for "no results".
|
|
926
|
+
- "No results" → show empty state in results area, not an input error.
|
|
927
|
+
|
|
928
|
+
```tsx
|
|
929
|
+
<input type="search" />
|
|
930
|
+
<button aria-label="Clear search">×</button>
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
---
|
|
934
|
+
|
|
935
|
+
### PasswordInput
|
|
936
|
+
|
|
937
|
+
**Applies to: `PasswordInput`**
|
|
938
|
+
|
|
939
|
+
- Always include show/hide toggle.
|
|
940
|
+
- Show strength indicator + requirements list for new passwords.
|
|
941
|
+
- Validate confirm password on blur of confirm field only.
|
|
942
|
+
- `Revealed` state: type switches from `password` to `text`.
|
|
943
|
+
|
|
944
|
+
```tsx
|
|
945
|
+
<input type="password" autocomplete="current-password" />
|
|
946
|
+
// or new-password for registration
|
|
947
|
+
<button aria-label="Show password" /> // toggles to "Hide password"
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
---
|
|
951
|
+
|
|
952
|
+
### OTPInput
|
|
953
|
+
|
|
954
|
+
**Applies to: `OTPInput`**
|
|
955
|
+
|
|
956
|
+
- Auto-advance: entering a character moves focus to next field.
|
|
957
|
+
- Auto-submit: when last field filled (fixed length), trigger verification automatically.
|
|
958
|
+
- Paste handling: full code must populate all fields at once.
|
|
959
|
+
- Backspace: moves focus to previous field and clears it.
|
|
960
|
+
|
|
961
|
+
States: `Empty` · `Partially filled` · `Verifying` · `Correct` · `Error` · `Disabled`
|
|
962
|
+
|
|
963
|
+
```tsx
|
|
964
|
+
<div role="group" aria-label="Enter 6-digit verification code">
|
|
965
|
+
<input aria-label="Digit 1 of 6" />
|
|
966
|
+
<input aria-label="Digit 2 of 6" />
|
|
967
|
+
...
|
|
968
|
+
</div>
|
|
969
|
+
// Verifying state: aria-live announcement
|
|
970
|
+
// Correct/Error states: must be announced
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
---
|
|
974
|
+
|
|
975
|
+
### NumberInput
|
|
976
|
+
|
|
977
|
+
**Applies to: `NumberInput`**
|
|
978
|
+
|
|
979
|
+
- Always define and enforce min/max where applicable.
|
|
980
|
+
- Stepper buttons disable at boundaries.
|
|
981
|
+
- Always allow direct keyboard entry (steppers are not the only input method).
|
|
982
|
+
- Do NOT use for currency, ranges (use Slider), or large numeric IDs.
|
|
983
|
+
|
|
984
|
+
---
|
|
985
|
+
|
|
986
|
+
### PhoneInput
|
|
987
|
+
|
|
988
|
+
**Applies to: `PhoneInput`**
|
|
989
|
+
|
|
990
|
+
- Always use `PhoneInput` for international phone numbers (not a plain TextInput).
|
|
991
|
+
- Pre-select user's locale as default country code.
|
|
992
|
+
- Allow paste of full international number — auto-populate country selector and number field.
|
|
993
|
+
- Uses `PopoverWrapper` for dropdown (do not re-implement positioning).
|
|
994
|
+
|
|
995
|
+
```tsx
|
|
996
|
+
<div role="group" aria-label="Phone number">
|
|
997
|
+
<select aria-label="Country code" autocomplete="tel-country-code" />
|
|
998
|
+
<input aria-label="Phone number" autocomplete="tel" />
|
|
999
|
+
</div>
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
---
|
|
1003
|
+
|
|
1004
|
+
### Select
|
|
1005
|
+
|
|
1006
|
+
**Applies to: `Select`**
|
|
1007
|
+
|
|
1008
|
+
- Use for single-value selection from a fixed list when 7+ options or space-constrained.
|
|
1009
|
+
- Do NOT use for ≤6 options with space → use Radio instead.
|
|
1010
|
+
- Always include a placeholder option that is NOT a valid value.
|
|
1011
|
+
- Add search/filter within dropdown for 20+ options.
|
|
1012
|
+
|
|
1013
|
+
```tsx
|
|
1014
|
+
// Native when possible
|
|
1015
|
+
<select aria-required={required} aria-invalid={hasError}>
|
|
1016
|
+
<option value="">Select country</option>
|
|
1017
|
+
</select>
|
|
1018
|
+
|
|
1019
|
+
// Custom implementation
|
|
1020
|
+
<div role="combobox" aria-expanded={open} aria-haspopup="listbox" aria-controls="list">
|
|
1021
|
+
<div id="list" role="listbox">
|
|
1022
|
+
<div role="option" aria-selected="true">Portugal</div>
|
|
1023
|
+
</div>
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
---
|
|
1027
|
+
|
|
1028
|
+
### AutocompleteMulti
|
|
1029
|
+
|
|
1030
|
+
**Applies to: `AutocompleteMulti`**
|
|
1031
|
+
|
|
1032
|
+
- Search-and-select multiple values from large/dynamic lists.
|
|
1033
|
+
- Each selected value becomes a chip inside the input.
|
|
1034
|
+
- Backspace on empty field removes last chip.
|
|
1035
|
+
|
|
1036
|
+
```tsx
|
|
1037
|
+
<div role="combobox" aria-multiselectable="true" aria-expanded={open}>
|
|
1038
|
+
<button aria-label="Remove Portugal">×</button>
|
|
1039
|
+
<div role="listbox">
|
|
1040
|
+
<div role="option" aria-selected="true">Portugal</div>
|
|
1041
|
+
</div>
|
|
1042
|
+
// Chip add/remove: aria-live="polite"
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
---
|
|
1046
|
+
|
|
1047
|
+
### HorizontalInput
|
|
1048
|
+
|
|
1049
|
+
**Applies to: `HorizontalInput`**
|
|
1050
|
+
|
|
1051
|
+
- Label left, control right — layout wrapper only.
|
|
1052
|
+
- Use in dense settings panels, property editors.
|
|
1053
|
+
- Do NOT use in standard forms, mobile layouts, or with long labels.
|
|
1054
|
+
- Label must not wrap — keep to 3–4 words maximum.
|
|
1055
|
+
- Consistent label widths within a group.
|
|
1056
|
+
|
|
1057
|
+
---
|
|
1058
|
+
|
|
1059
|
+
### Slider
|
|
1060
|
+
|
|
1061
|
+
**Applies to: `Slider`**
|
|
1062
|
+
|
|
1063
|
+
- `Max value` type: one handle
|
|
1064
|
+
- `Range` type: two handles — prevent handles from crossing
|
|
1065
|
+
- Always show current value alongside handle (tooltip or persistent label).
|
|
1066
|
+
- Display min/max labels at track ends.
|
|
1067
|
+
- Pair with NumberInput when precision matters.
|
|
1068
|
+
|
|
1069
|
+
```tsx
|
|
1070
|
+
<div aria-label="Price range" aria-labelledby="slider-label">
|
|
1071
|
+
<input role="slider"
|
|
1072
|
+
aria-valuenow={120} aria-valuemin={0} aria-valuemax={500}
|
|
1073
|
+
aria-valuetext="€120"
|
|
1074
|
+
aria-label="Minimum price" />
|
|
1075
|
+
</div>
|
|
1076
|
+
// Arrow keys: ±1 step
|
|
1077
|
+
// Page Up/Down: larger increment
|
|
1078
|
+
// Home/End: jump to min/max
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
---
|
|
1082
|
+
|
|
1083
|
+
### UploadArea
|
|
1084
|
+
|
|
1085
|
+
**Applies to: `UploadArea`**
|
|
1086
|
+
|
|
1087
|
+
- Always provide click-to-browse fallback (not all users can drag-drop).
|
|
1088
|
+
- State accepted file types and size limits in Empty state.
|
|
1089
|
+
- Reject invalid file types immediately on drop with clear error.
|
|
1090
|
+
- Show upload progress after file accepted.
|
|
1091
|
+
|
|
1092
|
+
```tsx
|
|
1093
|
+
<div role="button" aria-label="Upload files — click or drag and drop"
|
|
1094
|
+
aria-describedby="upload-constraints" tabIndex={0}>
|
|
1095
|
+
// Enter/Space when focused: opens system file picker
|
|
1096
|
+
// Dropping state: aria-live announcement
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
---
|
|
1100
|
+
|
|
1101
|
+
### ChipsInput
|
|
1102
|
+
|
|
1103
|
+
**Applies to: `ChipsInput`**
|
|
1104
|
+
|
|
1105
|
+
- Free-form entry where each value becomes a chip.
|
|
1106
|
+
- Confirm with `Enter` (or comma/Tab as defined per product).
|
|
1107
|
+
- Validate each value before converting to chip.
|
|
1108
|
+
- Backspace on empty field removes last chip.
|
|
1109
|
+
|
|
1110
|
+
```tsx
|
|
1111
|
+
// Each chip remove button
|
|
1112
|
+
<button aria-label="Remove javascript">×</button>
|
|
1113
|
+
// Chip additions/removals
|
|
1114
|
+
aria-live="polite"
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
---
|
|
1118
|
+
|
|
1119
|
+
### DatePicker / DateRangePicker
|
|
1120
|
+
|
|
1121
|
+
**Applies to: `DateTimePicker`**
|
|
1122
|
+
|
|
1123
|
+
- `Single date`: selects one date
|
|
1124
|
+
- `Date range`: start + end date — start cannot be after end
|
|
1125
|
+
|
|
1126
|
+
#### Display modes
|
|
1127
|
+
- **Standalone**: always visible
|
|
1128
|
+
- **Popover**: triggered by clicking input field (uses `PopoverWrapper`)
|
|
1129
|
+
|
|
1130
|
+
#### Rules
|
|
1131
|
+
- Show human-readable format in trigger input: "15 Jan 2026" not "2026-01-15"
|
|
1132
|
+
- Communicate date constraints before user opens calendar, not just inside it
|
|
1133
|
+
- For range: show both months side by side when range spans months
|
|
1134
|
+
- Do NOT pre-select today for historical date entry (e.g. date of birth)
|
|
1135
|
+
|
|
1136
|
+
```tsx
|
|
1137
|
+
<input aria-haspopup="dialog" aria-expanded={open} />
|
|
1138
|
+
<div role="dialog" aria-label="Choose date range">
|
|
1139
|
+
<div role="grid">
|
|
1140
|
+
<div role="gridcell" aria-label="Monday, 15 January 2026" aria-selected="true" tabIndex={0} />
|
|
1141
|
+
<div role="gridcell" aria-label="Tuesday, 16 January 2026" aria-disabled="true" tabIndex={-1} />
|
|
1142
|
+
</div>
|
|
1143
|
+
<button aria-label="Previous month">‹</button>
|
|
1144
|
+
<button aria-label="Next month">›</button>
|
|
1145
|
+
</div>
|
|
1146
|
+
// Arrow keys: move between days
|
|
1147
|
+
// Enter: select day
|
|
1148
|
+
// Page Up/Down: navigate months
|
|
1149
|
+
// Home/End: first/last day of month
|
|
1150
|
+
// Single tabbable day with roving focus — not one tabstop per cell
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
---
|
|
1154
|
+
|
|
1155
|
+
### Spinner
|
|
1156
|
+
|
|
1157
|
+
**Applies to: `Spinner`**
|
|
1158
|
+
|
|
1159
|
+
- Use when operation is in progress and duration is unknown.
|
|
1160
|
+
- Sizes: `sm` (inline button) · `lg` (panels) · `xl` (page sections) · `xxl` (full-page)
|
|
1161
|
+
- Always pair with a label when standalone: "Loading…", "Saving…"
|
|
1162
|
+
- Do NOT use multiple spinners simultaneously — consolidate into one.
|
|
1163
|
+
|
|
1164
|
+
```tsx
|
|
1165
|
+
<div role="status" aria-label="Loading results">
|
|
1166
|
+
<svg aria-hidden="true" />
|
|
1167
|
+
</div>
|
|
1168
|
+
// Completion: aria-live="polite"
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
---
|
|
1172
|
+
|
|
1173
|
+
### SkeletonLoading
|
|
1174
|
+
|
|
1175
|
+
**Applies to: `SkeletonLoading`**
|
|
1176
|
+
|
|
1177
|
+
- Use when content takes >~300ms to load and shape is known.
|
|
1178
|
+
- Types: `Avatar` · `Pill` · `Image` · `Card`
|
|
1179
|
+
- Always animate (shimmer/pulse) — never show static skeleton shapes.
|
|
1180
|
+
- Replace all skeletons simultaneously when content is ready — no piecemeal replacement.
|
|
1181
|
+
|
|
1182
|
+
```tsx
|
|
1183
|
+
<div aria-busy="true" aria-label="Loading">
|
|
1184
|
+
<div aria-hidden="true" class="skeleton-avatar" />
|
|
1185
|
+
<div aria-hidden="true" class="skeleton-pill" />
|
|
1186
|
+
</div>
|
|
1187
|
+
// When content ready: aria-busy="false"
|
|
1188
|
+
```
|
|
1189
|
+
|
|
1190
|
+
---
|
|
1191
|
+
|
|
1192
|
+
### ProgressBar / LoadingBar
|
|
1193
|
+
|
|
1194
|
+
**Applies to: `ProgressBar`, `LoadingBar`**
|
|
1195
|
+
|
|
1196
|
+
- `LoadingBar`: page-level navigation only, top of viewport. Never inline.
|
|
1197
|
+
- `ProgressBar`: measured progress (file upload, onboarding). Always show percentage or step count alongside bar.
|
|
1198
|
+
|
|
1199
|
+
```tsx
|
|
1200
|
+
<div role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={60}
|
|
1201
|
+
aria-label="Uploading file" />
|
|
1202
|
+
// Completion: aria-live="polite"
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
---
|
|
1206
|
+
|
|
1207
|
+
### Scrollbar
|
|
1208
|
+
|
|
1209
|
+
**Applies to: `Scrollbar`**
|
|
1210
|
+
|
|
1211
|
+
- Never hide scrollbar entirely in scrollable areas.
|
|
1212
|
+
- Do not use both vertical and horizontal scrollbars on same container unless content is truly 2D.
|
|
1213
|
+
- Custom scrollbars are visual overlays only — underlying scroll must remain keyboard accessible.
|
|
1214
|
+
|
|
1215
|
+
```tsx
|
|
1216
|
+
// Scrollable container
|
|
1217
|
+
<div tabIndex={0} /> // allows keyboard focus and arrow key scrolling
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
---
|
|
1221
|
+
|
|
1222
|
+
### Shortcut
|
|
1223
|
+
|
|
1224
|
+
**Applies to: `Shortcut`**
|
|
1225
|
+
|
|
1226
|
+
- Non-interactive display element only.
|
|
1227
|
+
- Sizes: `sm` (tooltips/menus) · `md` (standalone/help panels)
|
|
1228
|
+
- Colors: `Default` (light BG) · `Inverted` (dark BG)
|
|
1229
|
+
- Use platform-standard notation. Detect platform — do not mix Mac/Windows symbols.
|
|
1230
|
+
- Keep to keys only: `⌘S` not `⌘ Save`
|
|
1231
|
+
|
|
1232
|
+
```tsx
|
|
1233
|
+
// Key symbols not reliably announced by screen readers
|
|
1234
|
+
<kbd aria-label="Command S">⌘S</kbd>
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
---
|
|
1238
|
+
|
|
1239
|
+
## Cross-component patterns
|
|
1240
|
+
|
|
1241
|
+
These patterns are **decision rules**. Apply before selecting a component.
|
|
1242
|
+
|
|
1243
|
+
### Pattern 1 — Single select
|
|
1244
|
+
|
|
1245
|
+
```
|
|
1246
|
+
Does selection require explicit confirmation?
|
|
1247
|
+
Yes → Radio (regardless of option count)
|
|
1248
|
+
No → How many options?
|
|
1249
|
+
2–3 → Segment control
|
|
1250
|
+
4+ → Select
|
|
1251
|
+
|
|
1252
|
+
Select has 20+ items?
|
|
1253
|
+
Yes → add search box inside dropdown
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
### Pattern 2 — Multi select
|
|
1257
|
+
|
|
1258
|
+
```
|
|
1259
|
+
How many options?
|
|
1260
|
+
Up to 3 → Chips (shown upfront, immediately toggleable)
|
|
1261
|
+
4+ → Select dropdown + Choice list (Checkbox variant)
|
|
1262
|
+
|
|
1263
|
+
Option set is large (20+), dynamic, or needs typing to find?
|
|
1264
|
+
→ Autocomplete multi
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
### Pattern 3 — Binary (on/off)
|
|
1268
|
+
|
|
1269
|
+
```
|
|
1270
|
+
Context?
|
|
1271
|
+
Toolbar, icon-only, immediate → Toggle button (+ mandatory Tooltip)
|
|
1272
|
+
Form, deferred, confirmed on submit → Checkbox
|
|
1273
|
+
Settings, labeled, immediate → Switcher
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
### Pattern 4 — Filter bar
|
|
1277
|
+
|
|
1278
|
+
```
|
|
1279
|
+
How many filter values?
|
|
1280
|
+
1 → Toggle button
|
|
1281
|
+
2–5 → Chips in a horizontal bar
|
|
1282
|
+
6+ → "Filters" button → dropdown (6–12) or drawer (13+)
|
|
1283
|
+
|
|
1284
|
+
Always: show active state, provide "Clear all" action when any filter active
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
### Pattern 5 — Pagination vs Infinite scroll
|
|
1288
|
+
|
|
1289
|
+
```
|
|
1290
|
+
Content type?
|
|
1291
|
+
Data tables → Pagination
|
|
1292
|
+
Feeds → Infinite scroll
|
|
1293
|
+
|
|
1294
|
+
Never mix patterns within the same view.
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
### Pattern 6 — Loading state
|
|
1298
|
+
|
|
1299
|
+
```
|
|
1300
|
+
Shape of loading content known?
|
|
1301
|
+
Yes → Skeleton loading
|
|
1302
|
+
No → Spinner
|
|
1303
|
+
|
|
1304
|
+
Scope?
|
|
1305
|
+
Full page navigation → Loading bar
|
|
1306
|
+
Full page/panel content → Skeleton or Spinner (xl/xxl)
|
|
1307
|
+
Section/card within page → Skeleton or Spinner (lg)
|
|
1308
|
+
Inline within a button → Spinner (sm), button disabled
|
|
1309
|
+
Background (user not waiting) → No indicator; Toast on completion
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
### Pattern 7 — Overlay
|
|
1313
|
+
|
|
1314
|
+
```
|
|
1315
|
+
User must act before continuing?
|
|
1316
|
+
Yes:
|
|
1317
|
+
Critical/destructive confirmation → Alert dialog
|
|
1318
|
+
Other task or form → Modal
|
|
1319
|
+
No:
|
|
1320
|
+
Content anchored to an element?
|
|
1321
|
+
Yes → Popover (rich) or Tooltip (plain text, 1 line)
|
|
1322
|
+
No → Toast (notification) or Drawer (extended task)
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
### Pattern 8 — Empty state
|
|
1326
|
+
|
|
1327
|
+
```
|
|
1328
|
+
Context size?
|
|
1329
|
+
Primary page content area → lg (illustration + title + description + CTA)
|
|
1330
|
+
Card/panel/section → sm (icon + title + description + CTA)
|
|
1331
|
+
|
|
1332
|
+
Cause?
|
|
1333
|
+
First use (nothing created yet) → Invite. CTA to create first item.
|
|
1334
|
+
Filtered empty (no results) → "No results for '[query]'" + Clear filters CTA
|
|
1335
|
+
Permission restricted → Explain restriction + contact admin
|
|
1336
|
+
Error (failed to load) → See Error pattern; Retry CTA
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
### Pattern 9 — Error
|
|
1340
|
+
|
|
1341
|
+
```
|
|
1342
|
+
Error scope?
|
|
1343
|
+
Single form field → Inline validation message
|
|
1344
|
+
Section within a page → Error pattern (inline, embedded)
|
|
1345
|
+
Entire page/flow blocked → Error screen (full-screen replacement)
|
|
1346
|
+
```
|
|
1347
|
+
|
|
1348
|
+
### Pattern 10 — Success feedback
|
|
1349
|
+
|
|
1350
|
+
```
|
|
1351
|
+
Action significance?
|
|
1352
|
+
Routine (save, update, delete) → Toast (Success, auto-dismiss 4–5s)
|
|
1353
|
+
Significant milestone/flow end → Success screen (Celebration or Confirmation)
|
|
1354
|
+
```
|
|
1355
|
+
|
|
1356
|
+
### Pattern 11 — Tabs vs Segment control (use both tests)
|
|
1357
|
+
|
|
1358
|
+
1. Does each option have a distinct content panel? → **Tabs**
|
|
1359
|
+
2. Does switching change how the same content is rendered? → **Segment control**
|
|
1360
|
+
3. Could each option exist as its own sub-page/route? → **Tabs**
|
|
1361
|
+
4. Is this a display preference, not an architectural division? → **Segment control**
|
|
1362
|
+
|
|
1363
|
+
---
|
|
1364
|
+
|
|
1365
|
+
## Navigation & Page Structure Rules
|
|
1366
|
+
|
|
1367
|
+
These rules define how pages are structured, how navigation behaves, and where
|
|
1368
|
+
actions are placed. They apply to every screen built with DS-Nagarro components.
|
|
1369
|
+
Apply these BEFORE selecting or placing any component.
|
|
1370
|
+
|
|
1371
|
+
---
|
|
1372
|
+
|
|
1373
|
+
### NAV-01: AppShell is mandatory
|
|
1374
|
+
Every page MUST use `AppShell` as its root layout component.
|
|
1375
|
+
Never build page layout manually with divs.
|
|
1376
|
+
✅ `<AppShell header={...} sidebar={...}>...</AppShell>`
|
|
1377
|
+
❌ `<div style={{ display: 'flex' }}>...</div>`
|
|
1378
|
+
|
|
1379
|
+
---
|
|
1380
|
+
|
|
1381
|
+
### NAV-02: Sidebar structure
|
|
1382
|
+
The Sidebar always follows this exact structure — top to bottom:
|
|
1383
|
+
1. **Workspace switcher** — top of sidebar
|
|
1384
|
+
2. **Main navigation items** — middle (product-specific)
|
|
1385
|
+
3. **User profile + Settings** — bottom of sidebar
|
|
1386
|
+
|
|
1387
|
+
Never deviate from this order. Never place navigation items above the workspace switcher.
|
|
1388
|
+
|
|
1389
|
+
---
|
|
1390
|
+
|
|
1391
|
+
### NAV-03: Back arrows — top-level pages
|
|
1392
|
+
NEVER show back/forward navigation arrows in the Navbar on top-level section pages.
|
|
1393
|
+
Top-level pages are direct sidebar navigation items (e.g. Users, Reports, Pipeline).
|
|
1394
|
+
✅ Users page → no back arrow
|
|
1395
|
+
✅ Reports page → no back arrow
|
|
1396
|
+
❌ Users page → back arrow shown
|
|
1397
|
+
|
|
1398
|
+
---
|
|
1399
|
+
|
|
1400
|
+
### NAV-04: Back arrows — detail views
|
|
1401
|
+
ONLY show back arrow in Navbar when the user has navigated INTO a detail view
|
|
1402
|
+
(i.e. a record, sub-page, or child of a top-level section).
|
|
1403
|
+
✅ Alice Johnson's profile page → show back arrow
|
|
1404
|
+
✅ Report detail page → show back arrow
|
|
1405
|
+
❌ Top-level Users list → no back arrow
|
|
1406
|
+
|
|
1407
|
+
---
|
|
1408
|
+
|
|
1409
|
+
### NAV-05: Navbar title — two modes
|
|
1410
|
+
|
|
1411
|
+
**Mode 1 — Top-level page:**
|
|
1412
|
+
Show plain section title in the Navbar. No breadcrumbs.
|
|
1413
|
+
✅ Navbar: "Users"
|
|
1414
|
+
✅ Navbar: "Reports"
|
|
1415
|
+
|
|
1416
|
+
**Mode 2 — Detail view (2+ levels deep):**
|
|
1417
|
+
Show large `Breadcrumbs` component in the Navbar INSTEAD of a plain title.
|
|
1418
|
+
The breadcrumb IS the title — do not show both.
|
|
1419
|
+
The current page name (rightmost item) is bold and non-interactive.
|
|
1420
|
+
Parent items are interactive links.
|
|
1421
|
+
|
|
1422
|
+
✅ Navbar breadcrumb: "Users > **Alice Johnson**"
|
|
1423
|
+
❌ Navbar title: "Alice Johnson" + separate breadcrumb below it
|
|
1424
|
+
❌ Navbar title: "Users" when inside Alice Johnson's page
|
|
1425
|
+
|
|
1426
|
+
---
|
|
1427
|
+
|
|
1428
|
+
### NAV-06: Breadcrumb variants — two levels
|
|
1429
|
+
|
|
1430
|
+
There are two breadcrumb sizes and they serve different purposes:
|
|
1431
|
+
|
|
1432
|
+
| Variant | Where | Purpose |
|
|
1433
|
+
|---|---|---|
|
|
1434
|
+
| **Large** (`BreadcrumbsController`) | Navbar | Main navigation hierarchy — replaces page title on detail views |
|
|
1435
|
+
| **Small** (`Breadcrumbs`) | Inside page content | Sub-section hierarchy within a detail page |
|
|
1436
|
+
|
|
1437
|
+
NEVER use small breadcrumbs for main navigation.
|
|
1438
|
+
NEVER use large breadcrumbs inside content areas.
|
|
1439
|
+
|
|
1440
|
+
✅ Navbar: large breadcrumb "Users > Alice Johnson"
|
|
1441
|
+
✅ Inside content: small breadcrumb "Permissions > Edit role"
|
|
1442
|
+
❌ Navbar: small breadcrumb
|
|
1443
|
+
❌ Content area: large breadcrumb
|
|
1444
|
+
|
|
1445
|
+
---
|
|
1446
|
+
|
|
1447
|
+
### NAV-07: Breadcrumb format
|
|
1448
|
+
Format: `[Parent section] > [Current page]`
|
|
1449
|
+
- Parent items: interactive links
|
|
1450
|
+
- Current page (rightmost): non-interactive, visually bold
|
|
1451
|
+
- Do NOT always prepend root/Home — start from the immediate meaningful parent
|
|
1452
|
+
- Never truncate the current page item
|
|
1453
|
+
|
|
1454
|
+
✅ "Users > Alice Johnson"
|
|
1455
|
+
✅ "Reports > Q4 2025"
|
|
1456
|
+
❌ "Home > Users > Alice Johnson" (unnecessary root)
|
|
1457
|
+
❌ "Alice Johnson" alone (missing parent context)
|
|
1458
|
+
|
|
1459
|
+
---
|
|
1460
|
+
|
|
1461
|
+
### NAV-08: Primary CTA placement — top-level pages
|
|
1462
|
+
The primary action for a section always sits in the Navbar, to the right of the title.
|
|
1463
|
+
NEVER place the primary CTA inside a Card header or content area on a top-level page.
|
|
1464
|
+
|
|
1465
|
+
Primary actions are object-creation actions:
|
|
1466
|
+
"New user", "New deal", "Create report", "New email"
|
|
1467
|
+
|
|
1468
|
+
✅ Navbar: "Users" + [New user — Primary button]
|
|
1469
|
+
❌ Card header: [New user — Primary button] on a top-level page
|
|
1470
|
+
|
|
1471
|
+
---
|
|
1472
|
+
|
|
1473
|
+
### NAV-09: CTA hierarchy in Navbar
|
|
1474
|
+
When a page has multiple actions, use this hierarchy in the Navbar (right side):
|
|
1475
|
+
|
|
1476
|
+
| Action type | Button variant | Example |
|
|
1477
|
+
|---|---|---|
|
|
1478
|
+
| Primary object creation | `Primary` | "New deal", "New user" |
|
|
1479
|
+
| Important but not primary | `Secondary` | "Import", "Publish" |
|
|
1480
|
+
| Utility / contextual actions | `Ghost` | "Share", "Edit", "Export" |
|
|
1481
|
+
|
|
1482
|
+
Order right-to-left by importance: Ghost → Secondary → Primary
|
|
1483
|
+
(Primary is always the rightmost — trailing edge)
|
|
1484
|
+
|
|
1485
|
+
✅ [Export — Ghost] [Share — Ghost] [Edit — Secondary] [New user — Primary]
|
|
1486
|
+
❌ [New user — Primary] [Share — Ghost] (wrong order)
|
|
1487
|
+
❌ Two Primary buttons in the same Navbar
|
|
1488
|
+
|
|
1489
|
+
---
|
|
1490
|
+
|
|
1491
|
+
### NAV-10: Child section CTAs
|
|
1492
|
+
When a detail page has sub-sections with their own primary actions,
|
|
1493
|
+
those actions belong at the sub-section level — NOT in the top Navbar.
|
|
1494
|
+
|
|
1495
|
+
✅ Alice Johnson page → Navbar: [Save changes — Primary]
|
|
1496
|
+
✅ Alice Johnson > Permissions tab → tab-level: [Add permission — Primary]
|
|
1497
|
+
❌ Alice Johnson page → Navbar has both "Save changes" AND "Add permission"
|
|
1498
|
+
|
|
1499
|
+
---
|
|
1500
|
+
|
|
1501
|
+
### NAV-11: Section headers inside content
|
|
1502
|
+
Use the `SectionHeader` component to divide content sections within a page.
|
|
1503
|
+
Section headers are standalone dividers — they do NOT need to be inside a Card.
|
|
1504
|
+
Description line is optional — use it when the section needs clarification,
|
|
1505
|
+
omit it when the title is self-explanatory.
|
|
1506
|
+
|
|
1507
|
+
✅ SectionHeader "Personal info" (no description needed)
|
|
1508
|
+
✅ SectionHeader "Permissions" + description "Control what this user can access"
|
|
1509
|
+
❌ Wrapping every section in a Card just to have a title
|
|
1510
|
+
❌ Using plain `<h2>` or `<h3>` instead of SectionHeader component
|
|
1511
|
+
|
|
1512
|
+
---
|
|
1513
|
+
|
|
1514
|
+
### NAV-12: Page title and section header casing
|
|
1515
|
+
ALL page titles, Navbar titles, breadcrumb items, and section headers use sentence case.
|
|
1516
|
+
NEVER title case.
|
|
1517
|
+
|
|
1518
|
+
✅ "Personal info", "New user", "Alice Johnson", "Save changes"
|
|
1519
|
+
❌ "Personal Info", "New User", "Save Changes"
|
|
1520
|
+
|
|
1521
|
+
---
|
|
1522
|
+
|
|
1523
|
+
### NAV-13: Cards vs sections
|
|
1524
|
+
Not every content group needs a Card.
|
|
1525
|
+
Use a Card when the content is a distinct, self-contained unit that benefits
|
|
1526
|
+
from visual elevation or grouping (e.g. a data table, a form block, a summary panel).
|
|
1527
|
+
Use SectionHeader + flat content when sections are part of a continuous page flow.
|
|
1528
|
+
|
|
1529
|
+
✅ DataTable inside a Card
|
|
1530
|
+
✅ SectionHeader "Personal info" → flat form fields below (no Card)
|
|
1531
|
+
❌ Every section wrapped in a Card by default
|
|
1532
|
+
|
|
1533
|
+
---
|
|
1534
|
+
|
|
1535
|
+
## Layout & Component Behavior Rules
|
|
1536
|
+
|
|
1537
|
+
These rules apply globally. Apply them before building any screen or component.
|
|
1538
|
+
|
|
1539
|
+
---
|
|
1540
|
+
|
|
1541
|
+
### LCB-01: AppShell scroll — only the content area scrolls
|
|
1542
|
+
The sidebar and navbar are always fixed. Only the main content area scrolls.
|
|
1543
|
+
Never apply scroll to the full page or the AppShell root.
|
|
1544
|
+
|
|
1545
|
+
✅ `<AppShell>` — sidebar and navbar fixed; content area overflows and scrolls independently
|
|
1546
|
+
❌ Full page scrolls, taking the sidebar and navbar with it
|
|
1547
|
+
|
|
1548
|
+
---
|
|
1549
|
+
|
|
1550
|
+
### LCB-02: Data visualisation — always use dataviz tokens
|
|
1551
|
+
All charts, graphs, and data visualisations must use `--color-dataviz-*` tokens from `tokens.css` for all colors (series, axes, labels, backgrounds).
|
|
1552
|
+
Never use hardcoded hex values or generic semantic tokens for dataviz color.
|
|
1553
|
+
|
|
1554
|
+
✅ `fill: var(--color-dataviz-1)`
|
|
1555
|
+
❌ `fill: #0F766E`
|
|
1556
|
+
❌ `fill: var(--background-accent)`
|
|
1557
|
+
|
|
1558
|
+
---
|
|
1559
|
+
|
|
1560
|
+
### LCB-03: Horizontal spacing — always use `--page-margin-x`
|
|
1561
|
+
All horizontal spacing in the content area must use `var(--page-margin-x)`.
|
|
1562
|
+
This applies to every container without exception: pages, cards, modals, drawers, tables, lists, and form sections.
|
|
1563
|
+
Never introduce independent `padding-left`, `padding-right`, `margin-left`, or `margin-right` values that deviate from this token.
|
|
1564
|
+
|
|
1565
|
+
✅ `padding-inline: var(--page-margin-x)` on content areas, modals, drawers, cards
|
|
1566
|
+
❌ `padding: 24px` or `margin: 0 16px` hardcoded on any container
|
|
1567
|
+
❌ Different horizontal spacing values across containers causing misalignment
|
|
1568
|
+
|
|
1569
|
+
---
|
|
1570
|
+
|
|
1571
|
+
### LCB-04: Card internal padding — content must never touch card edges
|
|
1572
|
+
Every card must apply internal padding using the correct inset token.
|
|
1573
|
+
Content must never touch the card border.
|
|
1574
|
+
|
|
1575
|
+
✅ `padding: var(--inset-large)` inside every card
|
|
1576
|
+
❌ Card content flush with card edges
|
|
1577
|
+
|
|
1578
|
+
---
|
|
1579
|
+
|
|
1580
|
+
### LCB-05: Icons — always use Lucide, never system icons
|
|
1581
|
+
The only permitted icon library is Lucide (`lucide-react`).
|
|
1582
|
+
Never use browser/OS-native icons, emoji, or icons from any other library.
|
|
1583
|
+
This applies everywhere: tables, buttons, inputs, menus, empty states, and all other components.
|
|
1584
|
+
|
|
1585
|
+
✅ `import { ArrowUpDown } from 'lucide-react'`
|
|
1586
|
+
❌ System sort icon (↕) in table column headers
|
|
1587
|
+
❌ Any icon not from the Lucide library
|
|
1588
|
+
|
|
1589
|
+
---
|
|
1590
|
+
|
|
1591
|
+
### LCB-06: List items — always stretch to full container width
|
|
1592
|
+
List items inside any container (modal, drawer, card, page section) must stretch to fill the full available width.
|
|
1593
|
+
Never let list items float at an arbitrary width or leave unexplained whitespace to the right.
|
|
1594
|
+
|
|
1595
|
+
✅ List item `width: 100%` of container (minus `--page-margin-x` padding)
|
|
1596
|
+
❌ List items sized to content, leaving empty space on the right
|
|
1597
|
+
|
|
1598
|
+
---
|
|
1599
|
+
|
|
1600
|
+
### LCB-07: Placeholder content — never leave it in place
|
|
1601
|
+
Component slots for icons, help text, and hint icons must never contain placeholder values.
|
|
1602
|
+
Either replace with real, meaningful content or hide the slot entirely.
|
|
1603
|
+
|
|
1604
|
+
This applies to:
|
|
1605
|
+
- **Icons** inside inputs, selects, and buttons → replace with a relevant Lucide icon, or hide
|
|
1606
|
+
- **Help text** → replace with genuinely useful guidance, or hide
|
|
1607
|
+
- **Help/hint icons** → replace with meaningful tooltip content, or hide
|
|
1608
|
+
|
|
1609
|
+
✅ Help text: "We'll use this to send you login notifications."
|
|
1610
|
+
✅ Icon slot hidden when no meaningful icon applies
|
|
1611
|
+
❌ Help text reads "Help text"
|
|
1612
|
+
❌ Placeholder diamond icon left inside a Select component
|
|
1613
|
+
|
|
1614
|
+
---
|
|
1615
|
+
|
|
1616
|
+
### LCB-08: Required fields — use "Optional" label, not asterisk
|
|
1617
|
+
Every form field is mandatory by default.
|
|
1618
|
+
Never use a red asterisk (`*`) to mark required fields.
|
|
1619
|
+
Only mark optional fields, using an "Optional" label styled in `var(--text-tertiary)`, placed inline after the field label.
|
|
1620
|
+
|
|
1621
|
+
✅ `Team <span style="color: var(--text-tertiary)">Optional</span>`
|
|
1622
|
+
✅ Fields with no qualifier → assumed mandatory
|
|
1623
|
+
❌ `First name *` with a red asterisk
|
|
1624
|
+
❌ `aria-required="true"` shown visually as an asterisk
|
|
1625
|
+
|
|
1626
|
+
---
|
|
1627
|
+
|
|
1628
|
+
### LCB-09: Form CTA — disabled until mandatory fields have a value
|
|
1629
|
+
The primary submit button in any form (modal, drawer, page) must be disabled until all mandatory fields contain a value.
|
|
1630
|
+
On submit, validate all fields and display all errors simultaneously.
|
|
1631
|
+
Do not validate in real time as the user types — only on submit attempt.
|
|
1632
|
+
|
|
1633
|
+
Exceptions where real-time validation is acceptable: password strength, character count limits, search/filter fields, OTP.
|
|
1634
|
+
|
|
1635
|
+
✅ Primary CTA disabled → user fills all mandatory fields → button enables → submit → validate all
|
|
1636
|
+
❌ Primary CTA active on an empty form
|
|
1637
|
+
❌ Inline errors appearing as the user types in a standard form field
|
|
1638
|
+
|
|
1639
|
+
---
|
|
1640
|
+
|
|
1641
|
+
### LCB-10: Form submit keyboard shortcut — always show `⌘↵` on primary CTA
|
|
1642
|
+
The primary action button in every form must display the `⌘↵` keyboard shortcut hint using the `Shortcut` component.
|
|
1643
|
+
`Cmd+Enter` is the standard submit shortcut across all forms.
|
|
1644
|
+
|
|
1645
|
+
✅ `<Button variant="primary">Add user <Shortcut>⌘↵</Shortcut></Button>`
|
|
1646
|
+
❌ Primary form button with no keyboard shortcut hint
|
|
1647
|
+
|
|
1648
|
+
---
|
|
1649
|
+
|
|
1650
|
+
### Tag semantic color mapping
|
|
1651
|
+
|
|
1652
|
+
Always match Tag variant to the semantic meaning of the value. Never use the
|
|
1653
|
+
same variant for all tags in a list.
|
|
1654
|
+
|
|
1655
|
+
| Value meaning | Tag variant |
|
|
1656
|
+
|---------------------------|--------------|
|
|
1657
|
+
| Done, active, on track, success, approved, complete | success |
|
|
1658
|
+
| At risk, pending, in review, in progress, waiting | warning |
|
|
1659
|
+
| Behind, blocked, failed, rejected, overdue, error | error |
|
|
1660
|
+
| Draft, inactive, unknown, archived, neutral | neutral |
|
|
1661
|
+
| Informational, new, upcoming | info |
|
|
1662
|
+
|
|
1663
|
+
When in doubt, ask: is this good, bad, cautionary, or neutral?
|
|
1664
|
+
Pick the variant that matches that meaning.
|
|
1665
|
+
|
|
1666
|
+
---
|
|
1667
|
+
|
|
1668
|
+
### Page margin application
|
|
1669
|
+
|
|
1670
|
+
The content area must always have horizontal breathing room. Apply
|
|
1671
|
+
var(--page-margin-x) as padding-inline on the direct child of the content area —
|
|
1672
|
+
not on individual components.
|
|
1673
|
+
|
|
1674
|
+
/* CORRECT */
|
|
1675
|
+
.page-content {
|
|
1676
|
+
padding-inline: var(--page-margin-x);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
/* WRONG */
|
|
1680
|
+
.page-content {
|
|
1681
|
+
padding: 0; /* flush to edges */
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
This applies to every page without exception: tables, cards, charts, lists,
|
|
1685
|
+
empty states. Nothing should ever touch the left or right edge of the content area.
|
|
1686
|
+
|
|
1687
|
+
---
|
|
1688
|
+
|
|
1689
|
+
## Copy & content guidelines
|
|
1690
|
+
|
|
1691
|
+
### Buttons
|
|
1692
|
+
- Start with a strong verb: "Save", "Delete", "Export", "Send", "Create"
|
|
1693
|
+
- Be specific: "Save changes" not "OK" or "Yes"
|
|
1694
|
+
- 1–3 words; 5 maximum
|
|
1695
|
+
- Sentence case: "Save changes" not "Save Changes"
|
|
1696
|
+
- Never truncate with ellipsis — rewrite shorter
|
|
1697
|
+
- Never use first-person pronouns: "Save changes" not "Save my changes"
|
|
1698
|
+
- Destructive: name what is destroyed. "Delete account" not "Confirm"
|
|
1699
|
+
- Loading: present-progressive. "Saving…" "Sending…"
|
|
1700
|
+
|
|
1701
|
+
### Labels (inputs, checkboxes, radios, switchers)
|
|
1702
|
+
- Noun or noun phrase describing the value/setting
|
|
1703
|
+
- Sentence case: "Phone number" not "Phone Number"
|
|
1704
|
+
- Never describe the action: "Email address" not "Enter your email address"
|
|
1705
|
+
- Describe the thing being controlled, not the act of toggling: "Email notifications" not "Turn on email notifications"
|
|
1706
|
+
|
|
1707
|
+
### Placeholder text
|
|
1708
|
+
- Example of expected format, not a label: "e.g. john@example.com"
|
|
1709
|
+
- Never as the only label — it disappears on input
|
|
1710
|
+
- Must be clearly distinguishable from user-entered values
|
|
1711
|
+
|
|
1712
|
+
### Error messages
|
|
1713
|
+
- Full sentences. Full stop at the end.
|
|
1714
|
+
- Specific and instructive: what went wrong + how to fix it
|
|
1715
|
+
- "Email address is invalid" not "Invalid input"
|
|
1716
|
+
- "Password must include at least one uppercase letter" not "Invalid password"
|
|
1717
|
+
- Do not blame the user: "Please enter a valid email address" not "You entered an invalid email address"
|
|
1718
|
+
- Include "please" when giving an instruction
|
|
1719
|
+
|
|
1720
|
+
| Situation | Pattern | Example |
|
|
1721
|
+
|---|---|---|
|
|
1722
|
+
| Required field empty | Please enter [field name]. | Please enter your first name. |
|
|
1723
|
+
| Too long | Please enter fewer than [N] characters. | Please enter fewer than 50 characters. |
|
|
1724
|
+
| Too short | Please enter at least [N] characters. | Please enter at least 8 characters. |
|
|
1725
|
+
| Wrong format | Please enter [field name] in the correct format. | Please enter your email in the correct format. |
|
|
1726
|
+
| System fault | Sorry, [what happened]. Please [action]. | Sorry, we couldn't save your changes. Please try again. |
|
|
1727
|
+
|
|
1728
|
+
### Toasts
|
|
1729
|
+
- Lead with outcome: "Changes saved" not "We are saving your changes"
|
|
1730
|
+
- Past tense for completed actions: "Exported", "Deleted"
|
|
1731
|
+
- Present tense for ongoing states: "Connection lost"
|
|
1732
|
+
- No "Successfully" prefix: "File uploaded" not "Successfully uploaded file"
|
|
1733
|
+
- One sentence max for Inline toast
|
|
1734
|
+
|
|
1735
|
+
### Tags
|
|
1736
|
+
- 1–3 words. Nouns or adjectives.
|
|
1737
|
+
- Sentence case: "In review" not "In Review"
|
|
1738
|
+
- Never abbreviate unless universally understood
|
|
1739
|
+
- Label and variant must tell the same story
|
|
1740
|
+
|
|
1741
|
+
### Chips
|
|
1742
|
+
- 1–3 words. Sentence case.
|
|
1743
|
+
- Nouns or adjectives. All chips in a group must be parallel in structure.
|
|
1744
|
+
- Never truncate with ellipsis — rewrite
|
|
1745
|
+
|
|
1746
|
+
### Tabs / Segment control
|
|
1747
|
+
- 1–3 words for tabs, 1–2 words for segment control
|
|
1748
|
+
- Nouns only. No verbs, no questions.
|
|
1749
|
+
- Sentence case.
|
|
1750
|
+
|
|
1751
|
+
### Empty states
|
|
1752
|
+
- Title: 2–5 words. No punctuation at end. Situation understandable from title alone.
|
|
1753
|
+
- Description: 1–2 lines. Full stop.
|
|
1754
|
+
- CTA button: 1–3 words. Verb-first. No full stop.
|
|
1755
|
+
- Never use: "Oops", "Uh oh", or phrases that trivialise the situation
|
|
1756
|
+
- For filtered empty: surface a clear "Clear filters" path
|
|
1757
|
+
|
|
1758
|
+
### Modal content
|
|
1759
|
+
- Title: 3–6 words. Sentence case. For confirmations: "Delete project" not "Are you sure?"
|
|
1760
|
+
- Body for confirmations: one sentence stating consequence and reversibility.
|
|
1761
|
+
- Footer primary action: specific verb matching title. "Delete" not "OK" or "Confirm".
|
|
1762
|
+
- Cancel: "Cancel" for reversible, "Close" for informational.
|
|
1763
|
+
|
|
1764
|
+
---
|
|
1765
|
+
|
|
1766
|
+
## Accessibility requirements
|
|
1767
|
+
|
|
1768
|
+
### Contrast
|
|
1769
|
+
- Normal text: WCAG AA minimum 4.5:1
|
|
1770
|
+
- Large text: 3:1 minimum
|
|
1771
|
+
- Focus rings: must pass WCAG AA contrast on the background they appear on
|
|
1772
|
+
|
|
1773
|
+
### Keyboard
|
|
1774
|
+
- All interactive elements must be keyboard operable.
|
|
1775
|
+
- Focus order: top to bottom, left to right (LTR)
|
|
1776
|
+
- Closed disclosure components (Accordion, Popover, Dropdown): children must NOT be keyboard-reachable until open.
|
|
1777
|
+
|
|
1778
|
+
### Focus
|
|
1779
|
+
- Always use `:focus-visible`, never `:focus` as the visible ring trigger.
|
|
1780
|
+
- Never `outline: none` without explicit replacement.
|
|
1781
|
+
- Focus must never be hidden by `overflow: hidden` or clipping on a parent.
|
|
1782
|
+
- Focus rings follow component's border radius.
|
|
1783
|
+
- Buttons/filled: offset ring using `var(--borders-focus-primary)`.
|
|
1784
|
+
- Inputs: border thickens to focus token when focused.
|
|
1785
|
+
- Destructive: use `var(--borders-focus-destructive)`.
|
|
1786
|
+
|
|
1787
|
+
### Disabled elements
|
|
1788
|
+
- Native controls: use `disabled` attribute → removed from tab order.
|
|
1789
|
+
- Custom controls: `tabIndex={-1}` + `aria-disabled="true"`.
|
|
1790
|
+
- Disabled elements must NOT be focusable.
|
|
1791
|
+
- Exception: if user needs to discover the control exists, keep focusable with explanatory `aria-describedby`.
|
|
1792
|
+
|
|
1793
|
+
### Color
|
|
1794
|
+
- Color alone must never convey meaning.
|
|
1795
|
+
- Pair with text, icon, or pattern for any meaning conveyed through color.
|
|
1796
|
+
|
|
1797
|
+
### Forms / Inputs
|
|
1798
|
+
- Every input must have a programmatic label (`<label for>`, `aria-label`, or `aria-labelledby`).
|
|
1799
|
+
- Helper text and error messages: associated via `aria-describedby`.
|
|
1800
|
+
- Error state: `aria-invalid="true"` on the control.
|
|
1801
|
+
- All inputs are mandatory by default — always add `aria-required="true"`. For optional fields, omit `aria-required` or set to `false`.
|
|
1802
|
+
- Do not use `autocomplete="off"` without strong justification.
|
|
1803
|
+
- Error messages must not disappear automatically — persist until error is resolved.
|
|
1804
|
+
|
|
1805
|
+
### Loading states
|
|
1806
|
+
- `aria-busy="true"` while loading. `aria-busy="false"` when complete.
|
|
1807
|
+
- Loading spinners: `aria-hidden="true"` on the graphic; the label carries the meaning.
|
|
1808
|
+
- Button loading: `aria-busy="true"` on the button.
|
|
1809
|
+
|
|
1810
|
+
### Live regions
|
|
1811
|
+
- Polite: `aria-live="polite"` — after current interaction completes (Default, Success, Warning toasts; spinner completion; chip changes).
|
|
1812
|
+
- Assertive: `aria-live="assertive"` — interrupts immediately (Error toasts).
|
|
1813
|
+
|
|
1814
|
+
### Popup / dropdown keyboard contract
|
|
1815
|
+
Every popup/dropdown must support:
|
|
1816
|
+
- `Arrow` keys: navigate between items
|
|
1817
|
+
- `Enter` / `Space`: select/activate
|
|
1818
|
+
- `Escape`: close and return focus to trigger
|
|
1819
|
+
- Focus return to trigger on close
|
|
1820
|
+
|
|
1821
|
+
### Custom widget keyboard contracts
|
|
1822
|
+
| Widget type | Required keyboard behavior |
|
|
1823
|
+
|---|---|
|
|
1824
|
+
| `menuitem` | Enter/Space to activate; Arrow keys to navigate |
|
|
1825
|
+
| `switch` | Space to toggle |
|
|
1826
|
+
| `checkbox` | Space to toggle |
|
|
1827
|
+
| `radio` | Arrow keys to move selection; Space to select (or moves on Arrow) |
|
|
1828
|
+
| `slider` | Arrow keys ±1 step; Page Up/Down larger; Home/End min/max |
|
|
1829
|
+
| `tab` | Arrow keys between tabs; Enter/Space to activate |
|
|
1830
|
+
| `gridcell` | Roving tabindex; Arrow keys to navigate; Enter to select |
|
|
1831
|
+
|
|
1832
|
+
### Testing requirements
|
|
1833
|
+
Every contribution must:
|
|
1834
|
+
- Pass `npm run test:a11y:keyboard-widgets`
|
|
1835
|
+
- Pass `npm run test:a11y:stories:light`
|
|
1836
|
+
- Pass `npm run test:a11y:stories:dark`
|
|
1837
|
+
- Have at least one keyboard interaction test for custom-role widgets
|
|
1838
|
+
- Verify disabled state tab-order
|
|
1839
|
+
- Verify focus-visible-only ring (never `:focus`)
|
|
1840
|
+
- Verify role/name/value for custom widgets
|
|
1841
|
+
|
|
1842
|
+
Story-level a11y opt-out (disabled/demo stories):
|
|
1843
|
+
```typescript
|
|
1844
|
+
// Acceptable — scoped to story level only
|
|
1845
|
+
export const DisabledVariant: Story = {
|
|
1846
|
+
parameters: { a11y: { disable: true } }
|
|
1847
|
+
// Add comment explaining intentional exception
|
|
1848
|
+
};
|
|
1849
|
+
// NEVER disable at component meta level or globally for interactive components
|
|
1850
|
+
```
|
|
1851
|
+
|
|
1852
|
+
---
|
|
1853
|
+
|
|
1854
|
+
## What NOT to do (consolidated don'ts)
|
|
1855
|
+
|
|
1856
|
+
### Tokens
|
|
1857
|
+
- ❌ Use primitive tokens in component styles. `var(--primitive-neutral-900)` in a button is wrong.
|
|
1858
|
+
- ❌ Hardcode hex values, pixel sizes, or rgba in styled components.
|
|
1859
|
+
- ❌ Reference `foreground` as a token category — it was renamed to `text`.
|
|
1860
|
+
- ❌ Use `base`, `normal`, or `rest` as the default state name — always use `default`.
|
|
1861
|
+
- ❌ Use `inset/` tokens for spacing between elements (use `space/`).
|
|
1862
|
+
- ❌ Use `space/` tokens for internal component padding (use `inset/`).
|
|
1863
|
+
- ❌ Use generic semantic tokens or hardcoded hex values for dataviz colors — always use `--color-dataviz-*`.
|
|
1864
|
+
|
|
1865
|
+
### TypeScript
|
|
1866
|
+
- ❌ Type props as `string` when a union is possible. `variant: string` is wrong; `variant: 'primary' | 'secondary'` is correct.
|
|
1867
|
+
- ❌ Accept `state` as a prop. States are CSS pseudo-classes and boolean props, not a state prop.
|
|
1868
|
+
- ❌ Forward Styled Component's styling props to the DOM. Use transient props (`$propName`).
|
|
1869
|
+
|
|
1870
|
+
### States
|
|
1871
|
+
- ❌ Use `opacity: 0.5` for disabled state. Use explicit disabled tokens: `var(--background-disabled)`, `var(--color-text-disabled)`.
|
|
1872
|
+
- ❌ Conditionally render different DOM elements for states.
|
|
1873
|
+
- ❌ Use `outline: none` without providing an explicit replacement.
|
|
1874
|
+
- ❌ Use `:focus` as the visible focus ring trigger — always `:focus-visible`.
|
|
1875
|
+
|
|
1876
|
+
### Buttons
|
|
1877
|
+
- ❌ Place two Primary buttons side by side (except forced mutually exclusive choice).
|
|
1878
|
+
- ❌ Use Toggle button as a substitute for Switcher.
|
|
1879
|
+
- ❌ Use `aria-pressed` on Main or Destructive buttons — only on Toggle/Vertical.
|
|
1880
|
+
- ❌ Use vague button labels: "OK", "Yes", "Click here", "Proceed".
|
|
1881
|
+
- ❌ Use same label for two buttons on the same screen that do different things.
|
|
1882
|
+
- ❌ Truncate a button label with ellipsis.
|
|
1883
|
+
- ❌ Auto-confirm destructive actions — always require an explicit user action.
|
|
1884
|
+
|
|
1885
|
+
### Components — decision errors
|
|
1886
|
+
- ❌ Use Badge when the label is text → use Tag.
|
|
1887
|
+
- ❌ Use Tag when the element is interactive → use Chip.
|
|
1888
|
+
- ❌ Use Chip when the element is purely informational → use Tag.
|
|
1889
|
+
- ❌ Use Checkbox when effect is immediate → use Switcher.
|
|
1890
|
+
- ❌ Use Switcher when effect requires confirmation → use Checkbox.
|
|
1891
|
+
- ❌ Use Radio for 7+ options → use Select.
|
|
1892
|
+
- ❌ Use Segment control for options with distinct content panels → use Tabs.
|
|
1893
|
+
- ❌ Use Tooltip when content is longer than one sentence → use Popover.
|
|
1894
|
+
- ❌ Use Tooltip when content has interactive elements → use Popover.
|
|
1895
|
+
- ❌ Open a Modal from within a Modal — nested modals break focus management.
|
|
1896
|
+
- ❌ Open a Popover from within a Popover — nested popovers create layering issues.
|
|
1897
|
+
- ❌ Auto-open modals on page load.
|
|
1898
|
+
- ❌ Use Toast for critical actions that require user response → use Modal.
|
|
1899
|
+
- ❌ Use Toast for inline form field validation → use inline validation.
|
|
1900
|
+
- ❌ Use Skeleton for instant operations (<300ms) — avoid flicker.
|
|
1901
|
+
- ❌ Use Spinner for operations where content shape is known → use Skeleton.
|
|
1902
|
+
- ❌ Use NumberInput for currency amounts or ranges → use Slider for ranges.
|
|
1903
|
+
- ❌ Use PasswordInput for PINs or OTPs → use OTPInput.
|
|
1904
|
+
- ❌ Use TextInput for multi-line → use TextArea.
|
|
1905
|
+
- ❌ Use Plain variant Button in `md` size — Plain is `sm` only.
|
|
1906
|
+
|
|
1907
|
+
### Layout and placement
|
|
1908
|
+
- ❌ Build page layout manually with divs — always use `AppShell`.
|
|
1909
|
+
- ❌ Apply scroll to the full page or AppShell root — only the content area scrolls.
|
|
1910
|
+
- ❌ Use hardcoded `padding-left`, `padding-right`, `margin-left`, or `margin-right` on any container — always use `var(--page-margin-x)`.
|
|
1911
|
+
- ❌ Allow card content to touch card edges — always apply `padding: var(--inset-large)`.
|
|
1912
|
+
- ❌ Leave list items undersized — always stretch to full container width.
|
|
1913
|
+
- ❌ Mix Tab and CustomView tab types in the same row.
|
|
1914
|
+
- ❌ Nest tabs within tabs.
|
|
1915
|
+
- ❌ Place pagination above content — always below.
|
|
1916
|
+
- ❌ Mix Pagination and infinite scroll patterns in the same view.
|
|
1917
|
+
- ❌ Place Breadcrumbs inside cards, modals, or drawers.
|
|
1918
|
+
- ❌ Place Loading bar inline within components — top of viewport only.
|
|
1919
|
+
- ❌ Place Segment control in the middle of body content.
|
|
1920
|
+
- ❌ Use both vertical and horizontal scrollbars on the same container (unless truly 2D content).
|
|
1921
|
+
- ❌ Float a lone button at an arbitrary position — anchor it to the content it acts on.
|
|
1922
|
+
- ❌ Place tags at the bottom of an item's hierarchy — always close to what they describe.
|
|
1923
|
+
- ❌ Show more than 3 tags on a single item.
|
|
1924
|
+
- ❌ Mix Avatar sizes within the same list or group.
|
|
1925
|
+
- ❌ Mix Chip sizes in the same group.
|
|
1926
|
+
- ❌ Mix Checkbox or Radio sizes in the same group.
|
|
1927
|
+
- ❌ Mix Switcher sizes in the same group.
|
|
1928
|
+
|
|
1929
|
+
### Icons and placeholder content
|
|
1930
|
+
- ❌ Use any icon library other than Lucide (`lucide-react`) — no system icons, no emoji, no other libraries.
|
|
1931
|
+
- ❌ Leave placeholder icons in component slots — replace with a meaningful Lucide icon or hide the slot.
|
|
1932
|
+
- ❌ Leave placeholder help text in place — replace with real content or hide entirely.
|
|
1933
|
+
- ❌ Leave placeholder hint icons in place — replace with meaningful tooltip content or hide.
|
|
1934
|
+
|
|
1935
|
+
### Forms
|
|
1936
|
+
- ❌ Mark required fields with a red asterisk — use an "Optional" label in `var(--text-tertiary)` for optional fields only.
|
|
1937
|
+
- ❌ Enable the primary CTA before all mandatory fields have a value.
|
|
1938
|
+
- ❌ Validate form fields in real time as the user types (except: password strength, character count, search, OTP).
|
|
1939
|
+
- ❌ Omit the `⌘↵` keyboard shortcut hint from the primary submit button.
|
|
1940
|
+
|
|
1941
|
+
### Content writing
|
|
1942
|
+
- ❌ Use "Oops", "Uh oh", or diminishing phrases in empty or error states.
|
|
1943
|
+
- ❌ Blame the user in error messages.
|
|
1944
|
+
- ❌ Show "0" badge — remove the badge when count is zero.
|
|
1945
|
+
- ❌ Show a badge with a number > 99 — show "99+".
|
|
1946
|
+
- ❌ Use placeholder text as the only label for an input.
|
|
1947
|
+
- ❌ Use "Successfully" as a toast prefix — just state the outcome.
|
|
1948
|
+
- ❌ Use "OK" or "Confirm" as primary action labels in modals — use specific verbs.
|
|
1949
|
+
- ❌ Use "Are you sure?" as a modal title — state what is being confirmed.
|
|
1950
|
+
- ❌ Abbreviate chip or tag labels unless universally understood.
|
|
1951
|
+
- ❌ Put interactive elements inside a Tooltip.
|
|
1952
|
+
|
|
1953
|
+
### Accessibility
|
|
1954
|
+
- ❌ Use color alone to convey meaning.
|
|
1955
|
+
- ❌ Leave disabled elements focusable (for custom controls without `disabled` attribute).
|
|
1956
|
+
- ❌ Allow focus to reach elements inside closed disclosure components.
|
|
1957
|
+
- ❌ Allow focus to escape a modal while it is open.
|
|
1958
|
+
- ❌ Suppress focus indicators.
|
|
1959
|
+
- ❌ Place essential information only in a tooltip — touch users will never see it.
|
|
1960
|
+
- ❌ Use `aria-label` alone when a visible label is present — link via `aria-labelledby` instead.
|
|
1961
|
+
- ❌ Use `aria-pressed` on non-toggle buttons.
|
|
1962
|
+
- ❌ Disable a11y checks at component meta or global level for interactive components.
|
|
1963
|
+
- ❌ Static (non-animated) skeleton shapes — must shimmer/pulse.
|
|
1964
|
+
- ❌ Replace skeleton placeholders one by one — replace all simultaneously.
|
|
1965
|
+
- ❌ Allow custom scrollbars to interfere with native keyboard scroll behavior.
|
|
1966
|
+
- ❌ Use `:focus` for OTP or any other component's visible ring — always `:focus-visible`.
|
|
1967
|
+
- ❌ Expose technical error codes, stack traces, or internal identifiers to users.
|
|
1968
|
+
|
|
1969
|
+
### Exports
|
|
1970
|
+
- ❌ Forget to export from component `index.ts`: `export { X } from './X'`
|
|
1971
|
+
- ❌ Forget to re-export from `src/index.ts`
|
|
1972
|
+
|
|
1973
|
+
---
|
|
1974
|
+
|
|
1975
|
+
*Generated from DS-Nagarro design system docs. Last updated: 2026-03-10.*
|
|
1976
|
+
*Source files: `docs/foundations.md`, `docs/ds-guidelines.md`, and all component docs in `docs/`.*
|