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