whale-igniter 1.3.0 → 1.3.1
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/dist/version.js +1 -1
- package/package.json +3 -1
- package/packs/atlas/instructions.md +3 -0
- package/packs/atlas/manifest.json +8 -0
- package/packs/atlas/rules.md +4 -0
- package/packs/forge/instructions.md +3 -0
- package/packs/forge/manifest.json +8 -0
- package/packs/forge/rules.md +4 -0
- package/packs/lighthouse/instructions.md +3 -0
- package/packs/lighthouse/manifest.json +8 -0
- package/packs/lighthouse/rules.md +4 -0
- package/packs/scribe/instructions.md +3 -0
- package/packs/scribe/manifest.json +8 -0
- package/packs/scribe/rules.md +4 -0
- package/packs/selene/instructions.md +3 -0
- package/packs/selene/manifest.json +7 -0
- package/packs/selene/rules.md +4 -0
- package/ui-references/INDEX.md +51 -0
- package/ui-references/data-display.md +160 -0
- package/ui-references/feedback.md +190 -0
- package/ui-references/forms.md +192 -0
- package/ui-references/layout.md +155 -0
- package/ui-references/navigation.md +188 -0
- package/ui-references/surface.md +188 -0
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = "1.3.
|
|
1
|
+
export const PACKAGE_VERSION = "1.3.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whale-igniter",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "CLI-first operational intelligence. Bootstraps and adopts AI-readable project context so agents like Claude Code, Codex and Cursor understand your design system from the first commit. Includes an MCP server, a file watcher, deterministic insights, and an opt-in AI bridge (Selene).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
12
|
"brand",
|
|
13
|
+
"packs",
|
|
14
|
+
"ui-references",
|
|
13
15
|
"README.md",
|
|
14
16
|
"LICENSE",
|
|
15
17
|
"docs/ROADMAP.md"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "atlas",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Product strategy, decision logs and roadmaps.",
|
|
5
|
+
"status": "stub",
|
|
6
|
+
"requiresAI": false,
|
|
7
|
+
"auditNote": "No generator or validator commands implemented. Decision logging is handled by whale decision (core), not this pack."
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lighthouse",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Accessibility audits, contrast and semantic validation.",
|
|
5
|
+
"status": "stub",
|
|
6
|
+
"requiresAI": false,
|
|
7
|
+
"auditNote": "Validators directory is empty. No runtime rules implemented in v1.3. Roadmapped for v1.4."
|
|
8
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# UI References — Index
|
|
2
|
+
|
|
3
|
+
This library is a curated set of component quality benchmarks drawn from the best production design systems. Consult it when building any UI component: before writing code to understand the quality bar, and when reviewing code to catch common mistakes.
|
|
4
|
+
|
|
5
|
+
AI agents: read this file first to locate the right category, then read that category file for component-specific guidance.
|
|
6
|
+
|
|
7
|
+
## Categories
|
|
8
|
+
|
|
9
|
+
| File | Covers | When to consult |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| [forms.md](forms.md) | Button, Input, Select, Checkbox, Radio, Toggle, Textarea | Any user input element |
|
|
12
|
+
| [navigation.md](navigation.md) | Nav bar, Tabs, Sidebar, Breadcrumb, Dropdown menu, Pagination, Command palette | Any wayfinding or navigation element |
|
|
13
|
+
| [feedback.md](feedback.md) | Toast, Modal, Alert, Progress, Loading states, Skeleton, Empty state | Any element that communicates system status |
|
|
14
|
+
| [surface.md](surface.md) | Card, Popover, Tooltip, Sheet, Avatar, Separator, Badge | Any container or overlay surface |
|
|
15
|
+
| [layout.md](layout.md) | Container, Stack, Grid, Responsive patterns, Sidebar layout | Any structural layout element |
|
|
16
|
+
| [data-display.md](data-display.md) | Table, List, Code block, Stats, Timeline | Any element that presents data |
|
|
17
|
+
|
|
18
|
+
## Minimum design system
|
|
19
|
+
|
|
20
|
+
A project is considered to have a design system when it has all of the following components implemented to the quality bar described in their respective files:
|
|
21
|
+
|
|
22
|
+
- **Button** — with default, hover, focus-visible, active, disabled, loading states and icon variant
|
|
23
|
+
- **Input** — with label, placeholder, error, disabled states and accessible labeling
|
|
24
|
+
- **Select** — keyboard-navigable, with accessible option list
|
|
25
|
+
- **Checkbox** and **Radio** — with indeterminate state for checkbox
|
|
26
|
+
- **Card** — as a reusable surface with consistent padding and radius
|
|
27
|
+
- **Modal / Dialog** — with focus trap, Escape to close, and accessible title
|
|
28
|
+
- **Toast / Notification** — dismissible, accessible, with distinct severity levels
|
|
29
|
+
|
|
30
|
+
These seven families are the minimum. Everything else is project-specific.
|
|
31
|
+
|
|
32
|
+
## How this library is structured
|
|
33
|
+
|
|
34
|
+
Each category file follows the same format per component:
|
|
35
|
+
|
|
36
|
+
1. One paragraph on what the component is and why it matters.
|
|
37
|
+
2. **Reference implementations** — two to four links to high-quality real-world implementations with one sentence on what each gets right.
|
|
38
|
+
3. **Minimum quality bar** — a checklist of states and behaviors the component must have to ship.
|
|
39
|
+
4. **Common mistakes** — a short list of patterns to avoid.
|
|
40
|
+
|
|
41
|
+
## Reference source tiers
|
|
42
|
+
|
|
43
|
+
When citing implementations, this library prioritizes:
|
|
44
|
+
|
|
45
|
+
- **Tier 1** (cited frequently): Linear, Vercel/Geist, Radix UI, shadcn/ui, Sonner, Headless UI, Park UI
|
|
46
|
+
- **Tier 2** (cited when category-appropriate): Material Design 3, Apple HIG, Carbon (IBM), Polaris (Shopify), Fluent (Microsoft), Atlassian, GitHub Primer
|
|
47
|
+
- **Tier 3** (cited for niche or emerging needs): Mantine, Ark UI, Origin UI, Animata
|
|
48
|
+
|
|
49
|
+
## Contributing
|
|
50
|
+
|
|
51
|
+
To add a component or update a reference link, edit the relevant category file directly. The format per component is fixed — add new components at the end of the file or insert them in frequency order. Do not use AI generation to create entries; references must point to real, stable implementations you have verified.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Data Display — UI References
|
|
2
|
+
|
|
3
|
+
Data display components present information that the user reads but does not directly edit. The quality bar is high: data must be scannable, sortable when appropriate, and never overflow or truncate silently. Accessibility concerns shift from interaction to comprehension: screen readers must be able to navigate tables and lists in a meaningful order.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table
|
|
8
|
+
|
|
9
|
+
A structured grid of rows and columns. Tables are for relational data where both the row and column identity matter. Do not use tables for layout.
|
|
10
|
+
|
|
11
|
+
### Reference implementations
|
|
12
|
+
|
|
13
|
+
- **[shadcn/ui Table](https://ui.shadcn.com/docs/components/table)** — semantic HTML (`<table>`, `<thead>`, `<tbody>`, `<th scope="col">`) with Tailwind styling; the correct baseline.
|
|
14
|
+
- **[Radix UI Table](https://www.radix-ui.com/themes/docs/components/table)** — themed table with built-in row hover, column alignment helpers, and condensed/standard density variants.
|
|
15
|
+
- **[TanStack Table](https://tanstack.com/table/latest)** — the leading headless table library; handles sorting, filtering, pagination, and virtual scrolling for large datasets. Consult for any table with more than a few dozen rows.
|
|
16
|
+
- **[Linear's issue list](https://linear.app)** — observe: not a `<table>` but a virtual list; shows how sorting, filtering, and row selection are handled in a list-as-table pattern common in modern apps.
|
|
17
|
+
|
|
18
|
+
### Minimum quality bar
|
|
19
|
+
|
|
20
|
+
- [ ] Semantic HTML: `<table>`, `<thead>`, `<tbody>`, `<tfoot>` (if total row), `<tr>`, `<th>`, `<td>`
|
|
21
|
+
- [ ] Column headers: `<th scope="col">` with a text description; not just icons
|
|
22
|
+
- [ ] Row headers (if applicable): `<th scope="row">` on the first cell of each row
|
|
23
|
+
- [ ] `<caption>` element or `aria-label` on the table describing what it contains
|
|
24
|
+
- [ ] Sortable columns: `aria-sort="ascending"` / `"descending"` / `"none"` on the column header
|
|
25
|
+
- [ ] Horizontal scroll on small viewports: wrap in `overflow-x: auto` container — do not collapse columns silently
|
|
26
|
+
- [ ] Row selection: checkbox in first column; `aria-checked` on the row (or the checkbox); keyboard: Space to toggle row
|
|
27
|
+
- [ ] Sticky header: `position: sticky; top: 0` — works with `overflow-x: auto` using the right scroll container
|
|
28
|
+
- [ ] Empty state: a single full-width `<td colspan="N">` with an empty state component, not a blank table
|
|
29
|
+
|
|
30
|
+
### Common mistakes
|
|
31
|
+
|
|
32
|
+
- Using `<div>` grid for tabular data — loses semantic meaning; screen readers cannot navigate by row/column
|
|
33
|
+
- Sortable columns with only visual state and no `aria-sort` — screen reader users can't tell the current sort
|
|
34
|
+
- Truncating cell content silently with `overflow: hidden` — use `text-overflow: ellipsis` and a tooltip for the full value
|
|
35
|
+
- No mobile strategy — either allow horizontal scroll, stack into a card per row, or use a different component for small viewports
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## List
|
|
40
|
+
|
|
41
|
+
An ordered or unordered sequence of items. Lists are the most common data structure in UI: navigation items, search results, notifications, file trees. Quality lists are scannable and navigable.
|
|
42
|
+
|
|
43
|
+
### Reference implementations
|
|
44
|
+
|
|
45
|
+
- **[Radix UI List](https://www.radix-ui.com/themes/docs/components/data-list)** — key-value data list pattern; good for detail views and settings pages.
|
|
46
|
+
- **[GitHub Primer ActionList](https://primer.style/components/action-list/react/alpha)** — the most complete reference for a list of interactive items (like a menu, but also used for navigation and selection): dividers, header groups, leading/trailing visuals, single/multi-select.
|
|
47
|
+
- **[TanStack Virtual](https://tanstack.com/virtual/latest)** — for lists with >100 items; virtual scrolling renders only visible items.
|
|
48
|
+
|
|
49
|
+
### Minimum quality bar
|
|
50
|
+
|
|
51
|
+
- [ ] Semantic HTML: `<ul>` for unordered, `<ol>` for ordered — not `<div>` + `<div>`
|
|
52
|
+
- [ ] Each item is `<li>`; interactive items inside `<li>` use `<a>` or `<button>`, not the `<li>` itself
|
|
53
|
+
- [ ] List roles override when semantics matter: `role="listbox"` + `role="option"` for a selectable list
|
|
54
|
+
- [ ] Keyboard navigation for interactive lists: arrow keys navigate items (roving tabindex or `aria-activedescendant`)
|
|
55
|
+
- [ ] Selected item: `aria-selected="true"` and a visual highlight
|
|
56
|
+
- [ ] Long lists: virtual scrolling or pagination — do not render 1000+ DOM nodes
|
|
57
|
+
- [ ] Grouped lists: each group has a visible heading and `role="group"` with `aria-label` pointing to that heading
|
|
58
|
+
|
|
59
|
+
### Common mistakes
|
|
60
|
+
|
|
61
|
+
- `<ul>` with `list-style: none` without `role="list"` — some screen readers strip list semantics when list style is removed (add `role="list"` to restore)
|
|
62
|
+
- Interactive items as `<li>` with `onClick` — the `<li>` is not an interactive role; use `<button>` or `<a>` inside
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Code Block
|
|
67
|
+
|
|
68
|
+
A formatted display of code, command output, or configuration. Must preserve whitespace, be readable at any content length, and allow copying.
|
|
69
|
+
|
|
70
|
+
### Reference implementations
|
|
71
|
+
|
|
72
|
+
- **[shadcn/ui](https://ui.shadcn.com)** — observe the code blocks throughout the documentation: copy button in the top-right corner, syntax highlighting, scrollable horizontally, no word wrap.
|
|
73
|
+
- **[Vercel/Geist Code](https://vercel.com/geist/code)** — inline code and block code variants; the inline variant for single-line snippets within prose.
|
|
74
|
+
- **[Shiki](https://shiki.matsu.io/)** — the leading syntax highlighter for static output (no runtime needed); generates semantic token HTML. Use for server-rendered or build-time highlighted code.
|
|
75
|
+
|
|
76
|
+
### Minimum quality bar
|
|
77
|
+
|
|
78
|
+
- [ ] `<pre><code>` elements — `<pre>` preserves whitespace; `<code>` signals that the content is code
|
|
79
|
+
- [ ] `tabindex="0"` on `<pre>` so keyboard users can scroll the code block
|
|
80
|
+
- [ ] Horizontal scroll on overflow — never wrap code unless the language is prose-like
|
|
81
|
+
- [ ] Copy button in the top-right corner with visual confirmation ("Copied!")
|
|
82
|
+
- [ ] Language label visible (e.g. "TypeScript", "bash") for multi-snippet docs
|
|
83
|
+
- [ ] Dark mode: code blocks often look better with a dark background in both light and dark themes
|
|
84
|
+
- [ ] `aria-label` on the `<pre>` if the language label is not a `<caption>` equivalent
|
|
85
|
+
|
|
86
|
+
### Common mistakes
|
|
87
|
+
|
|
88
|
+
- `white-space: pre-wrap` on code — wraps long lines in a way that breaks indentation context
|
|
89
|
+
- No horizontal scroll (`overflow-x: hidden`) on code blocks — silently truncates code
|
|
90
|
+
- Copy button that disappears after animation — show "Copied!" for at least 1.5 seconds
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Stats / Metrics
|
|
95
|
+
|
|
96
|
+
A display of key numbers or metrics with labels and optional context (trend, change, unit). Common in dashboards, reports, and analytics views.
|
|
97
|
+
|
|
98
|
+
### Reference implementations
|
|
99
|
+
|
|
100
|
+
- **[shadcn/ui example: Dashboard](https://ui.shadcn.com/examples/dashboard)** — the stat card pattern: large number, label below, trend indicator (up/down arrow + percentage).
|
|
101
|
+
- **[Vercel/Geist](https://vercel.com/geist/components)** — observe the analytics pages: stat value, label, optional sparkline in the card.
|
|
102
|
+
- **[Polaris KPICard](https://polaris.shopify.com)** — handles trend indicators, unit suffixes, comparison to previous period.
|
|
103
|
+
|
|
104
|
+
### Minimum quality bar
|
|
105
|
+
|
|
106
|
+
- [ ] Number has sufficient contrast and type scale — stat values should be the largest text element in the card
|
|
107
|
+
- [ ] Unit is part of the label or the value (`12.4k`, `$1,234`, `94%`), not ambiguous
|
|
108
|
+
- [ ] Trend indicator (if present): arrow + percentage; green/red with a text label ("Up 12%") — never color-only
|
|
109
|
+
- [ ] Large numbers are formatted with locale-appropriate separators (`toLocaleString()` or `Intl.NumberFormat`)
|
|
110
|
+
- [ ] Loading state is a skeleton with the same dimensions as the loaded state
|
|
111
|
+
|
|
112
|
+
### Common mistakes
|
|
113
|
+
|
|
114
|
+
- Trend indicator that uses color alone (just a green or red number) — inaccessible; add an up/down arrow or "▲ 12%" text
|
|
115
|
+
- Stats that don't communicate the time period — `$12,400` means nothing without "last 30 days" or similar context
|
|
116
|
+
- Numbers that reflow unpredictably on value change — use `tabular-nums` font feature to prevent layout shift when digits change
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Timeline
|
|
121
|
+
|
|
122
|
+
A chronological sequence of events, displayed as a vertical list with connecting lines and time markers. Used in audit logs, activity feeds, version histories.
|
|
123
|
+
|
|
124
|
+
### Reference implementations
|
|
125
|
+
|
|
126
|
+
- **[GitHub's commit history](https://github.com)** — observe: timestamp, author, branch indicator; condensed view with expand; the connecting line between events.
|
|
127
|
+
- **[Linear activity log](https://linear.app)** — observe in issue detail: avatar + action description + relative time; grouped by day; expandable for details.
|
|
128
|
+
- **[Atlassian Activity Streams](https://atlassian.design)** — enterprise-quality timeline with status changes, user attribution, and grouping.
|
|
129
|
+
|
|
130
|
+
### Minimum quality bar
|
|
131
|
+
|
|
132
|
+
- [ ] Semantic: use `<ol>` (ordered list) — timeline events are ordered
|
|
133
|
+
- [ ] Each event: timestamp, actor (if applicable), event description
|
|
134
|
+
- [ ] Time shown as relative ("3 minutes ago") with absolute time in a tooltip (`title` attribute or `aria-label`)
|
|
135
|
+
- [ ] Connecting line is decorative (`aria-hidden`); the list semantic conveys the sequence
|
|
136
|
+
- [ ] Long timelines: load more on scroll or paginate — do not render hundreds of events on mount
|
|
137
|
+
- [ ] Events grouped by day: group heading is a visible date, `role="separator"` divider
|
|
138
|
+
|
|
139
|
+
### Common mistakes
|
|
140
|
+
|
|
141
|
+
- Connecting line as part of the layout that breaks when the viewport narrows
|
|
142
|
+
- Relative timestamps that never update — "5 minutes ago" should update as the user stays on the page
|
|
143
|
+
- No empty state for timelines with no history — "No activity yet" prevents confusion
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Badge / Tag (Data Context)
|
|
148
|
+
|
|
149
|
+
A small label attached to data items to communicate category, status, or type. See [surface.md](surface.md) for the component itself; this section covers data-display-specific usage patterns.
|
|
150
|
+
|
|
151
|
+
### Usage patterns in data display
|
|
152
|
+
|
|
153
|
+
- **Status badges in tables**: align all status badges in the same column; use a fixed-width column so the rest of the row doesn't shift when status changes.
|
|
154
|
+
- **Category tags in lists**: tags that filter the list when clicked become `<button>` elements, not `<span>` — clicking must have a visible effect.
|
|
155
|
+
- **Priority indicators**: if using colored dots or badges for priority (urgent/high/medium/low), always accompany color with a text label or icon — do not rely on color alone.
|
|
156
|
+
|
|
157
|
+
### Reference implementations
|
|
158
|
+
|
|
159
|
+
- **[Linear's priority + status labels](https://linear.app)** — observe in issue list: priority dot + label, status as a colored chip with text; keyboard accessible in filter panels.
|
|
160
|
+
- **[GitHub labels](https://github.com)** — colored label with text; filter behavior when clicked in issues list; editable in label management UI.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Feedback — UI References
|
|
2
|
+
|
|
3
|
+
Feedback components communicate system status. They answer the user's question "did that work?" They must be timely (not delayed), clear (no ambiguous language), and unobtrusive (not blocking work the user is trying to do). A toast that the user misses, a modal that takes five clicks to dismiss, or an error message with jargon all represent feedback component failures.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Toast / Notification
|
|
8
|
+
|
|
9
|
+
A brief, auto-dismissing message that confirms an action or communicates a system event. Appears without interrupting the user's current task.
|
|
10
|
+
|
|
11
|
+
### Reference implementations
|
|
12
|
+
|
|
13
|
+
- **[Sonner](https://sonner.emilkowal.ski/)** — the current best-in-class toast library: smooth enter/exit animations, stacking with expansion on hover, rich content with actions, correct `role="status"` for non-urgent messages and `role="alert"` for errors. Study the source for the animation and stacking patterns.
|
|
14
|
+
- **[shadcn/ui Toast (Sonner)](https://ui.shadcn.com/docs/components/sonner)** — Sonner integration with the shadcn/ui theming system.
|
|
15
|
+
- **[Radix UI Toast](https://www.radix-ui.com/primitives/docs/components/toast)** — alternative implementation; observe how it handles `swipeDirection` and the viewport positioning.
|
|
16
|
+
- **[Linear's toasts](https://linear.app)** — observe in the app: appear bottom-left, short duration, action button inline, stack without expanding (up to ~3 visible), oldest auto-dismisses when stack is full.
|
|
17
|
+
|
|
18
|
+
### Minimum quality bar
|
|
19
|
+
|
|
20
|
+
- [ ] Non-critical toasts: `role="status"`, `aria-live="polite"` — screen reader announces after current speech finishes
|
|
21
|
+
- [ ] Error/warning toasts: `role="alert"`, `aria-live="assertive"` — screen reader announces immediately
|
|
22
|
+
- [ ] Auto-dismiss after 4–6 seconds; longer for messages with actions
|
|
23
|
+
- [ ] Dismiss button always present; hover pauses auto-dismiss timer
|
|
24
|
+
- [ ] Stacking: at most 3–5 toasts visible; oldest dismiss when stack is full
|
|
25
|
+
- [ ] Positioned away from interactive elements (bottom corner, not covering nav)
|
|
26
|
+
- [ ] Keyboard: Tab reaches the toast's action/dismiss button while it is visible
|
|
27
|
+
- [ ] Mobile: full-width at bottom, swipe-to-dismiss gesture
|
|
28
|
+
|
|
29
|
+
### Common mistakes
|
|
30
|
+
|
|
31
|
+
- Using `alert` role for success messages — causes screen readers to interrupt unnecessarily
|
|
32
|
+
- Auto-dismissing error messages — errors must persist until acknowledged
|
|
33
|
+
- Toasts covering important page content (especially modals or forms mid-submission)
|
|
34
|
+
- No action button for "undo" actions — missed opportunity to recover from mistakes
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Modal / Dialog
|
|
39
|
+
|
|
40
|
+
A focused overlay that interrupts the user's workflow to demand attention or a decision. Use sparingly: modals block the background and demand immediate resolution.
|
|
41
|
+
|
|
42
|
+
### Reference implementations
|
|
43
|
+
|
|
44
|
+
- **[Radix UI Dialog](https://www.radix-ui.com/primitives/docs/components/dialog)** — the reference implementation: focus trap, `role="dialog"`, `aria-modal="true"`, Escape to close, scroll lock on body, correct overlay with `aria-hidden` on background content.
|
|
45
|
+
- **[shadcn/ui Dialog](https://ui.shadcn.com/docs/components/dialog)** — Radix Dialog with Tailwind; includes `DialogHeader`, `DialogTitle` (required), `DialogDescription` (recommended), `DialogFooter` pattern.
|
|
46
|
+
- **[Headless UI Dialog](https://headlessui.com/react/dialog)** — alternative, with the `initialFocus` ref pattern for controlling which element receives focus when the modal opens.
|
|
47
|
+
|
|
48
|
+
### Minimum quality bar
|
|
49
|
+
|
|
50
|
+
- [ ] `role="dialog"` and `aria-modal="true"` on the dialog container
|
|
51
|
+
- [ ] `aria-labelledby` pointing to the dialog title; `aria-describedby` pointing to the description
|
|
52
|
+
- [ ] Focus trap: Tab and Shift+Tab cycle only within the modal while open
|
|
53
|
+
- [ ] First focusable element receives focus on open (or explicitly set with `autoFocus`)
|
|
54
|
+
- [ ] Escape closes the modal; clicking the overlay closes it; focus returns to the trigger
|
|
55
|
+
- [ ] Body scroll is locked while modal is open; background content is `aria-hidden="true"`
|
|
56
|
+
- [ ] Destructive modal: the destructive action is not the default focused button
|
|
57
|
+
- [ ] Animation: fade + scale in, fade + scale out; respects `prefers-reduced-motion`
|
|
58
|
+
|
|
59
|
+
### Common mistakes
|
|
60
|
+
|
|
61
|
+
- Modal without a visible title — `DialogTitle` is required even if visually hidden; without it screen readers have no context
|
|
62
|
+
- Focus returning to page top (not the trigger) after modal closes
|
|
63
|
+
- Modals that cannot be dismissed without taking an action — always provide a way out
|
|
64
|
+
- Nested modals — almost always a design problem that should be solved differently
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Alert / Banner
|
|
69
|
+
|
|
70
|
+
An inline message that communicates status, warnings, or errors within the page content. Unlike toasts, alerts do not auto-dismiss and are integrated into the layout.
|
|
71
|
+
|
|
72
|
+
### Reference implementations
|
|
73
|
+
|
|
74
|
+
- **[shadcn/ui Alert](https://ui.shadcn.com/docs/components/alert)** — clean implementation with icon + title + description pattern; default and destructive variants.
|
|
75
|
+
- **[Radix UI Callout](https://www.radix-ui.com/themes/docs/components/callout)** — four severity levels (info, success, warning, error) with icon and text.
|
|
76
|
+
- **[Polaris Banner](https://polaris.shopify.com/components/feedback-indicators/banner)** — good reference for how to handle actionable alerts (with dismiss, with action button, with expansion for details).
|
|
77
|
+
- **[GitHub Primer Flash](https://primer.style/components/flash/react/alpha)** — persistent alerts with full-width option for page-level messages.
|
|
78
|
+
|
|
79
|
+
### Minimum quality bar
|
|
80
|
+
|
|
81
|
+
- [ ] Four severity levels: info, success, warning, error — visually distinct beyond color (icon, border, label)
|
|
82
|
+
- [ ] `role="alert"` only for genuinely urgent messages that need immediate announcement; `role="status"` for success and info
|
|
83
|
+
- [ ] Dismissible alerts have a close button with `aria-label="Dismiss"`
|
|
84
|
+
- [ ] Actionable alerts: action button is secondary style (not primary — the alert should not compete with the page's primary action)
|
|
85
|
+
- [ ] Error alerts that accompany form validation: positioned near (or above) the form, not at page top
|
|
86
|
+
|
|
87
|
+
### Common mistakes
|
|
88
|
+
|
|
89
|
+
- `role="alert"` on every informational message — causes constant interruptions for screen reader users
|
|
90
|
+
- Color alone to convey severity — red text, yellow text without icon or label are inaccessible
|
|
91
|
+
- Alerts that appear and disappear too quickly — users miss them or can't read the message
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Progress Indicator
|
|
96
|
+
|
|
97
|
+
Communicates how far along a process is. Two types: determinate (known progress) and indeterminate (working but no ETA).
|
|
98
|
+
|
|
99
|
+
### Reference implementations
|
|
100
|
+
|
|
101
|
+
- **[Radix UI Progress](https://www.radix-ui.com/primitives/docs/components/progress)** — `role="progressbar"`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax` correctly implemented; smooth transition on value change.
|
|
102
|
+
- **[shadcn/ui Progress](https://ui.shadcn.com/docs/components/progress)** — clean linear progress bar; the animated indeterminate variant shows the pattern.
|
|
103
|
+
|
|
104
|
+
### Minimum quality bar
|
|
105
|
+
|
|
106
|
+
- [ ] Determinate: `role="progressbar"` with `aria-valuenow`, `aria-valuemin="0"`, `aria-valuemax="100"`, `aria-valuetext` for human-readable labels ("Step 2 of 5")
|
|
107
|
+
- [ ] Indeterminate: `aria-valuenow` omitted; CSS animation for visual motion; respects `prefers-reduced-motion` (reduce animation, don't remove it)
|
|
108
|
+
- [ ] Multi-step progress: step labels visible at all steps; completed steps visually distinct from future steps
|
|
109
|
+
- [ ] Color: progress fill must pass 3:1 contrast against the track
|
|
110
|
+
|
|
111
|
+
### Common mistakes
|
|
112
|
+
|
|
113
|
+
- Indeterminate progress bar staying for too long — show estimated time after 3+ seconds or switch to a spinner with a status message
|
|
114
|
+
- Progress that jumps backwards (data re-loading or error recovery) — disorienting; hold the last value if you can't go backwards meaningfully
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Loading State
|
|
119
|
+
|
|
120
|
+
Communicates that the system is working. Must be immediate (appear within 100ms of an action) and honest (not appear when there is nothing to load).
|
|
121
|
+
|
|
122
|
+
### Reference implementations
|
|
123
|
+
|
|
124
|
+
- **[Vercel/Geist Spinner](https://vercel.com/geist/spinner)** — minimal spinner; used inline and as a page-level overlay; the spinning animation is smooth.
|
|
125
|
+
- **[Radix UI Spinner (Themes)](https://www.radix-ui.com/themes/docs/components/spinner)** — several sizes, accessible.
|
|
126
|
+
- **[Linear loading](https://linear.app)** — observe: spinner appears within 100ms, replaces content (not overlays it in most cases), disappears immediately when content is ready.
|
|
127
|
+
|
|
128
|
+
### Minimum quality bar
|
|
129
|
+
|
|
130
|
+
- [ ] Appears within 100ms of triggering the async operation
|
|
131
|
+
- [ ] `role="status"` and `aria-label="Loading"` so screen readers announce it
|
|
132
|
+
- [ ] Replaces or overlays only the content being loaded — not the entire page for partial updates
|
|
133
|
+
- [ ] `prefers-reduced-motion`: reduce animation speed or show a static icon instead of spinning
|
|
134
|
+
- [ ] Spinner disappears immediately when load completes — no artificial minimum duration
|
|
135
|
+
|
|
136
|
+
### Common mistakes
|
|
137
|
+
|
|
138
|
+
- Loading overlay covering the full viewport for a small update in one section
|
|
139
|
+
- Minimum loading time ("show spinner for at least 500ms") — adds latency without benefit
|
|
140
|
+
- No loading state at all for operations that take >300ms — users think nothing happened and click again
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Skeleton
|
|
145
|
+
|
|
146
|
+
A placeholder that previews the shape of content while it loads. More useful than a generic spinner for content-heavy layouts.
|
|
147
|
+
|
|
148
|
+
### Reference implementations
|
|
149
|
+
|
|
150
|
+
- **[shadcn/ui Skeleton](https://ui.shadcn.com/docs/components/skeleton)** — minimal base implementation: pulse animation, neutral background, matches the shape of the content it replaces.
|
|
151
|
+
- **[Polaris SkeletonPage](https://polaris.shopify.com/components/feedback-indicators/skeleton-page)** — full-page skeleton with SkeletonBodyText, SkeletonDisplayText, SkeletonThumbnail; good reference for compositional approach.
|
|
152
|
+
|
|
153
|
+
### Minimum quality bar
|
|
154
|
+
|
|
155
|
+
- [ ] Matches the approximate layout and shape of the real content
|
|
156
|
+
- [ ] Pulse animation (opacity oscillation) is gentler than shimmer (left-to-right gradient) for users with motion sensitivity
|
|
157
|
+
- [ ] `aria-hidden="true"` on skeleton elements — they are decorative; screen readers should not read them
|
|
158
|
+
- [ ] Use `aria-busy="true"` on the container while loading; `aria-busy="false"` when content is ready
|
|
159
|
+
- [ ] Skeleton does not shift layout when content loads — reserve space accurately
|
|
160
|
+
|
|
161
|
+
### Common mistakes
|
|
162
|
+
|
|
163
|
+
- Skeleton that looks nothing like the real content — causes layout shift and disorientation when content appears
|
|
164
|
+
- Skeleton persisting after content has loaded — even briefly, confusing to users
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Empty State
|
|
169
|
+
|
|
170
|
+
The state of a view when there is no data to display. One of the most neglected components, and one of the most important for user retention.
|
|
171
|
+
|
|
172
|
+
### Reference implementations
|
|
173
|
+
|
|
174
|
+
- **[Linear's empty states](https://linear.app)** — observe in fresh workspace: illustration or icon, clear heading, specific explanation of why it's empty, one actionable next step. Never just "No items found."
|
|
175
|
+
- **[Polaris EmptyState](https://polaris.shopify.com/components/layout-and-structure/empty-state)** — image + heading + description + primary action + (optional) secondary action; the structure to follow.
|
|
176
|
+
- **[GitHub empty states](https://github.com)** — observe in repositories, issues, projects: different copy for "nothing exists yet" vs "nothing matches your filter" — those are two different problems.
|
|
177
|
+
|
|
178
|
+
### Minimum quality bar
|
|
179
|
+
|
|
180
|
+
- [ ] Distinct copy for "nothing exists yet" vs "nothing matches current filters" — different user problems
|
|
181
|
+
- [ ] At least one call to action ("Create your first X", "Clear filters")
|
|
182
|
+
- [ ] Visual treatment: icon or illustration (not a blank white box)
|
|
183
|
+
- [ ] When filtered and empty: show what filter is active; offer "Clear all filters"
|
|
184
|
+
- [ ] Error state (couldn't load) is distinct from empty state (loaded but empty) — error needs a retry
|
|
185
|
+
|
|
186
|
+
### Common mistakes
|
|
187
|
+
|
|
188
|
+
- Generic "No results" with no context or action — leaves users stranded
|
|
189
|
+
- Empty state that looks like a bug — provide enough context that users trust the app is working correctly
|
|
190
|
+
- Same empty state for all situations — loading error, empty dataset, and filter-with-no-results are three different states
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# Forms — UI References
|
|
2
|
+
|
|
3
|
+
Form elements are the primary way users take action in an application. Poor form components are the leading cause of accessibility failures and the most common source of user frustration. Every form element must work without a mouse, communicate its state clearly, and never lose the user's input silently.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Button
|
|
8
|
+
|
|
9
|
+
The most-pressed element in any UI. A button communicates that something will happen immediately when activated. It is not a link (links navigate). It is not a div with an onClick.
|
|
10
|
+
|
|
11
|
+
### Reference implementations
|
|
12
|
+
|
|
13
|
+
- **[shadcn/ui Button](https://ui.shadcn.com/docs/components/button)** — clean variant system (default, destructive, outline, ghost, link), loading state pattern with spinner-in-place, correct use of `asChild` for polymorphic rendering without breaking semantics.
|
|
14
|
+
- **[Radix UI Button primitive](https://www.radix-ui.com/themes/docs/components/button)** — shows how to handle the icon variant with `gap` and `size` scaling; the size scale is worth copying verbatim.
|
|
15
|
+
- **[Vercel/Geist Button](https://vercel.com/geist/button)** — minimal, high-contrast, used in production at scale; the loading spinner that replaces label text (not appended to it) is the right pattern.
|
|
16
|
+
- **[Linear buttons](https://linear.app)** — observe in the app: focus-visible rings that match brand accent, 36px height control, icon-only buttons with tooltip, destructive confirmation pattern.
|
|
17
|
+
|
|
18
|
+
### Minimum quality bar
|
|
19
|
+
|
|
20
|
+
- [ ] Default, hover, focus-visible, active, disabled states are all visually distinct
|
|
21
|
+
- [ ] Loading state: spinner replaces or overlays label; button is `disabled` during load
|
|
22
|
+
- [ ] With-icon variant: icon + label with consistent gap (4–6px); icon-only variant with tooltip
|
|
23
|
+
- [ ] Destructive variant: red/danger color, distinct from default primary
|
|
24
|
+
- [ ] Touch target minimum 44×44px on mobile (use padding, not just height)
|
|
25
|
+
- [ ] `type="button"` is explicit; never rely on browser default type in a form context
|
|
26
|
+
- [ ] Keyboard: Enter and Space both activate; Tab reaches the button
|
|
27
|
+
- [ ] Disabled state uses the `disabled` attribute, not just visual styling
|
|
28
|
+
|
|
29
|
+
### Common mistakes
|
|
30
|
+
|
|
31
|
+
- Using `<div>` or `<a>` for buttons that trigger actions — breaks keyboard and screen reader access
|
|
32
|
+
- Removing the focus ring with `outline: none` without a replacement — kills keyboard navigation
|
|
33
|
+
- Disabling buttons without explaining why — trap users who don't understand the state
|
|
34
|
+
- Using color alone to distinguish destructive from default — fails color-blind users
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Input
|
|
39
|
+
|
|
40
|
+
A single-line text field. The workhorse of data entry. Quality inputs reduce errors by making their state unambiguous at a glance.
|
|
41
|
+
|
|
42
|
+
### Reference implementations
|
|
43
|
+
|
|
44
|
+
- **[shadcn/ui Input](https://ui.shadcn.com/docs/components/input)** — clean baseline with label separation, consistent height, and error state color; pairs well with the Form component for validation messaging.
|
|
45
|
+
- **[Radix UI Form](https://www.radix-ui.com/primitives/docs/components/form)** — the best reference for the full label + input + error message pattern with accessible ARIA wiring.
|
|
46
|
+
- **[Vercel/Geist Input](https://vercel.com/geist/input)** — prefix/suffix slot pattern (icon or text before/after the field) handled without hacks.
|
|
47
|
+
- **[Polaris TextField](https://polaris.shopify.com/components/selection-and-input/text-field)** — character count, multiline behavior, inline error, and help text all handled consistently.
|
|
48
|
+
|
|
49
|
+
### Minimum quality bar
|
|
50
|
+
|
|
51
|
+
- [ ] Label is a real `<label>` with `for` pointing to the input's `id` — not a placeholder
|
|
52
|
+
- [ ] Placeholder text is supplementary, not a label replacement; it disappears on type
|
|
53
|
+
- [ ] Error state: red border + error message below the field (not inside it); `aria-describedby` links them
|
|
54
|
+
- [ ] Disabled state: grayed, non-interactive; communicates not the same as read-only
|
|
55
|
+
- [ ] Read-only state: selectable text, different visual treatment from disabled
|
|
56
|
+
- [ ] Focus state: visible ring on `focus-visible`, not on `focus` (avoids focus ring on click)
|
|
57
|
+
- [ ] Prefix/suffix slots for icons or units (e.g. "$", "px") without breaking layout
|
|
58
|
+
- [ ] Mobile: correct `inputmode` and `type` attributes to trigger the right keyboard
|
|
59
|
+
|
|
60
|
+
### Common mistakes
|
|
61
|
+
|
|
62
|
+
- Using placeholder as label — fails when the field has a value; inaccessible to screen readers
|
|
63
|
+
- `outline: none` on focus without replacement — invisible focus indicator
|
|
64
|
+
- Showing error messages before the user has interacted — don't validate on mount
|
|
65
|
+
- Input width set to 100% everywhere — inputs should have a width appropriate to their expected content
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Select
|
|
70
|
+
|
|
71
|
+
A dropdown for choosing one option from a set. Native `<select>` is accessible but unstyled; custom selects require careful ARIA construction to match native behavior.
|
|
72
|
+
|
|
73
|
+
### Reference implementations
|
|
74
|
+
|
|
75
|
+
- **[Radix UI Select](https://www.radix-ui.com/primitives/docs/components/select)** — the canonical reference for building an accessible custom select: `combobox` role, `listbox` for options, arrow key navigation, type-to-filter. Copy the ARIA pattern, not just the visuals.
|
|
76
|
+
- **[shadcn/ui Select](https://ui.shadcn.com/docs/components/select)** — Radix Select with Tailwind styling; the search-within-select is in Combobox (different component).
|
|
77
|
+
- **[Headless UI Listbox](https://headlessui.com/react/listbox)** — alternative implementation with slightly different API; good for comparing two approaches to the same problem.
|
|
78
|
+
|
|
79
|
+
### Minimum quality bar
|
|
80
|
+
|
|
81
|
+
- [ ] Keyboard: Up/Down arrows navigate options; Enter/Space selects; Escape closes; Home/End jump to first/last
|
|
82
|
+
- [ ] When closed, shows selected value (not placeholder unless nothing is selected)
|
|
83
|
+
- [ ] Supports option groups (`optgroup` semantics in custom select)
|
|
84
|
+
- [ ] Long option lists: the dropdown scrolls; first visible option after typing matches
|
|
85
|
+
- [ ] Touch: tap opens overlay positioned relative to viewport, not overflowing
|
|
86
|
+
- [ ] `aria-required` and `aria-invalid` passed through when applicable
|
|
87
|
+
|
|
88
|
+
### Common mistakes
|
|
89
|
+
|
|
90
|
+
- Building a custom select with `<div>` and `onClick` only — breaks keyboard and screen reader
|
|
91
|
+
- Opening the dropdown on hover — wrong interaction model; selects open on click/keypress
|
|
92
|
+
- Closing the dropdown on scroll of the parent page — confuses users mid-selection
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Checkbox
|
|
97
|
+
|
|
98
|
+
A toggle for boolean state or for selecting items from a list. Use checkbox (not radio) when multiple selections are valid.
|
|
99
|
+
|
|
100
|
+
### Reference implementations
|
|
101
|
+
|
|
102
|
+
- **[Radix UI Checkbox](https://www.radix-ui.com/primitives/docs/components/checkbox)** — correct ARIA role, indeterminate state exposed as `data-state="indeterminate"`, animated check mark with CSS transition.
|
|
103
|
+
- **[shadcn/ui Checkbox](https://ui.shadcn.com/docs/components/checkbox)** — minimal, paired with label using the `FormControl` + `FormLabel` pattern worth copying.
|
|
104
|
+
- **[Headless UI Checkbox](https://headlessui.com/react/checkbox)** — headless approach showing all the keyboard event handling necessary.
|
|
105
|
+
|
|
106
|
+
### Minimum quality bar
|
|
107
|
+
|
|
108
|
+
- [ ] Three states: unchecked, checked, indeterminate (for select-all patterns)
|
|
109
|
+
- [ ] Label is clickable and associated with `<label>` or `aria-label`
|
|
110
|
+
- [ ] Focus ring visible on keyboard focus; not on click
|
|
111
|
+
- [ ] Space bar toggles in all browsers
|
|
112
|
+
- [ ] In a group: each checkbox is independently labeled
|
|
113
|
+
- [ ] Indeterminate state is set via JS (not HTML attribute) and communicated via `aria-checked="mixed"`
|
|
114
|
+
|
|
115
|
+
### Common mistakes
|
|
116
|
+
|
|
117
|
+
- Using indeterminate as a third state (it is not — it means "some children are checked")
|
|
118
|
+
- Making the hit area the checkbox icon only — label must be part of the hit area
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Radio
|
|
123
|
+
|
|
124
|
+
A single-selection control. When the user must choose exactly one option from a visible, bounded list. Use a Select for long lists (7+ options).
|
|
125
|
+
|
|
126
|
+
### Reference implementations
|
|
127
|
+
|
|
128
|
+
- **[Radix UI Radio Group](https://www.radix-ui.com/primitives/docs/components/radio-group)** — `role="radiogroup"` on the container, `role="radio"` on each item, arrow key navigation between options in the group (not Tab).
|
|
129
|
+
- **[shadcn/ui Radio Group](https://ui.shadcn.com/docs/components/radio-group)** — clean implementation with `RadioGroupItem` + `Label` pairing.
|
|
130
|
+
|
|
131
|
+
### Minimum quality bar
|
|
132
|
+
|
|
133
|
+
- [ ] Arrow keys navigate within the group; Tab moves past the group to the next focusable element
|
|
134
|
+
- [ ] Only one radio in the group receives Tab focus at a time (roving tabindex)
|
|
135
|
+
- [ ] All radios share the same `name` attribute (native) or group context
|
|
136
|
+
- [ ] Pre-selected option: one option should have a default unless the absence of a choice is meaningful
|
|
137
|
+
- [ ] Label is always to the right of the radio circle (left-to-right layouts)
|
|
138
|
+
|
|
139
|
+
### Common mistakes
|
|
140
|
+
|
|
141
|
+
- Replacing a radio group with separate checkboxes — removes the mutually-exclusive semantic
|
|
142
|
+
- Making all radios Tab-focusable independently — breaks keyboard navigation model
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Toggle / Switch
|
|
147
|
+
|
|
148
|
+
A binary control that takes effect immediately, without a submit action. Distinct from a checkbox: checkboxes submit a form, toggles apply instantly.
|
|
149
|
+
|
|
150
|
+
### Reference implementations
|
|
151
|
+
|
|
152
|
+
- **[Radix UI Switch](https://www.radix-ui.com/primitives/docs/components/switch)** — `role="switch"`, `aria-checked` updates immediately, smooth sliding animation that communicates the state transition.
|
|
153
|
+
- **[shadcn/ui Switch](https://ui.shadcn.com/docs/components/switch)** — good example of label-on-right with description below.
|
|
154
|
+
- **[Headless UI Switch](https://headlessui.com/react/switch)** — headless; useful for understanding what keyboard events need handling.
|
|
155
|
+
|
|
156
|
+
### Minimum quality bar
|
|
157
|
+
|
|
158
|
+
- [ ] `role="switch"` with `aria-checked`
|
|
159
|
+
- [ ] Space and Enter both toggle
|
|
160
|
+
- [ ] Visual change is animated (helps users understand the state change happened)
|
|
161
|
+
- [ ] Label clearly names what is being toggled (not just "On/Off")
|
|
162
|
+
- [ ] State change triggers immediately — no submit required
|
|
163
|
+
- [ ] Disabled state with explanation (tooltip or nearby text) when toggle is locked
|
|
164
|
+
|
|
165
|
+
### Common mistakes
|
|
166
|
+
|
|
167
|
+
- Using a checkbox styled as a toggle — breaks ARIA semantics; `role="switch"` is different
|
|
168
|
+
- Toggle that requires a Save button — breaks the convention; if it needs confirmation, use a modal
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Textarea
|
|
173
|
+
|
|
174
|
+
Multi-line text input. Same principles as Input but with a resizing dimension to manage.
|
|
175
|
+
|
|
176
|
+
### Reference implementations
|
|
177
|
+
|
|
178
|
+
- **[shadcn/ui Textarea](https://ui.shadcn.com/docs/components/textarea)** — `resize-y` allowed, correct row default, auto-grow variant.
|
|
179
|
+
- **[Polaris TextField (multiline)](https://polaris.shopify.com/components/selection-and-input/text-field)** — character count below the field with warning at 80% capacity.
|
|
180
|
+
|
|
181
|
+
### Minimum quality bar
|
|
182
|
+
|
|
183
|
+
- [ ] User can resize vertically (allow `resize-y`); horizontal resize is usually disruptive
|
|
184
|
+
- [ ] `rows` attribute sets a sensible default height (not 2 — at least 4)
|
|
185
|
+
- [ ] Character count visible when there is a limit
|
|
186
|
+
- [ ] Same error, disabled, read-only, focus states as Input
|
|
187
|
+
- [ ] Auto-growing textarea (optional but high quality): height expands with content up to a max
|
|
188
|
+
|
|
189
|
+
### Common mistakes
|
|
190
|
+
|
|
191
|
+
- Preventing all resize — users have different viewport and font sizes
|
|
192
|
+
- Setting `height: auto` without `min-height` — collapses to zero on empty value
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Layout — UI References
|
|
2
|
+
|
|
3
|
+
Layout components and patterns define how content is positioned, sized, and reflowed across viewports. They are the invisible scaffolding of every screen. Good layout is invisible — content feels natural and reachable. Bad layout manifests as content that overflows on mobile, pages that don't scale, or sidebar panels that get in the way.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Container
|
|
8
|
+
|
|
9
|
+
A centered, width-constrained wrapper that gives content a comfortable reading width and consistent horizontal margins. The outermost structural element on most pages.
|
|
10
|
+
|
|
11
|
+
### Reference implementations
|
|
12
|
+
|
|
13
|
+
- **[Vercel/Geist Container](https://vercel.com/geist/container)** — `max-width` variants (sm, md, lg, xl), centered with `margin: 0 auto`, horizontal padding that scales with viewport width. The pattern to copy for most web apps.
|
|
14
|
+
- **[Tailwind CSS Container](https://tailwindcss.com/docs/container)** — `container` class with `mx-auto` and responsive padding; a widely-used baseline.
|
|
15
|
+
|
|
16
|
+
### Minimum quality bar
|
|
17
|
+
|
|
18
|
+
- [ ] Maximum width is appropriate for the content type (prose: 65–75ch; app UI: 1280px)
|
|
19
|
+
- [ ] Horizontal padding on both sides that scales with viewport — at minimum 16px on mobile, 24–32px on desktop
|
|
20
|
+
- [ ] Centers with `margin: 0 auto` — never with absolute positioning
|
|
21
|
+
- [ ] Does not constrain child elements from filling their parent width
|
|
22
|
+
- [ ] Nested containers: inner containers should not re-apply the outer max-width
|
|
23
|
+
|
|
24
|
+
### Common mistakes
|
|
25
|
+
|
|
26
|
+
- Using 100vw for the container width — includes scrollbar width and causes horizontal overflow on Windows
|
|
27
|
+
- Container with no horizontal padding on mobile — content touches viewport edges
|
|
28
|
+
- Nesting full-width sections inside a container that then applies max-width to everything
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Stack
|
|
33
|
+
|
|
34
|
+
A one-dimensional layout that distributes children along a single axis (vertical or horizontal) with consistent spacing. The most frequent layout primitive.
|
|
35
|
+
|
|
36
|
+
### Reference implementations
|
|
37
|
+
|
|
38
|
+
- **[Every Layout — Stack](https://every-layout.dev/layouts/stack/)** — the canonical primitive: each child separated by a consistent gap, no global margin reset needed.
|
|
39
|
+
- **[Radix UI Flex](https://www.radix-ui.com/themes/docs/layout/flex)** — flex-based stack with `gap`, `direction`, `align`, `justify` props; useful as a design system primitive.
|
|
40
|
+
- **[Tailwind's flex/gap utilities](https://tailwindcss.com/docs/display#flex)** — `flex flex-col gap-4` is the practical implementation of a vertical stack in Tailwind.
|
|
41
|
+
|
|
42
|
+
### Minimum quality bar
|
|
43
|
+
|
|
44
|
+
- [ ] Spacing between children uses `gap`, not margin on individual children — `gap` does not collapse and doesn't affect the outermost children
|
|
45
|
+
- [ ] Direction is explicit: vertical stack uses `flex-col`; horizontal uses `flex-row`
|
|
46
|
+
- [ ] Wrapping behavior is explicit: decide whether children wrap to the next row or clip
|
|
47
|
+
- [ ] Alignment: cross-axis alignment (`align-items`) defaults to `stretch` — be intentional about changing it
|
|
48
|
+
- [ ] Nested stacks use their own `gap` — do not inherit from a parent
|
|
49
|
+
|
|
50
|
+
### Common mistakes
|
|
51
|
+
|
|
52
|
+
- Using margin-bottom on children for spacing — breaks the last-child, creates double margins when nesting
|
|
53
|
+
- Gap on a non-flex/non-grid element — `gap` only works on flex and grid containers
|
|
54
|
+
- Using negative margin to "fix" unwanted spacing from margins — indicates a structural problem, not a spacing one
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Grid
|
|
59
|
+
|
|
60
|
+
A two-dimensional layout for aligning content in rows and columns. Use for card grids, form field layouts, and any content that requires both row and column alignment.
|
|
61
|
+
|
|
62
|
+
### Reference implementations
|
|
63
|
+
|
|
64
|
+
- **[Every Layout — Grid](https://every-layout.dev/layouts/grid/)** — the auto-fill/minmax pattern for responsive grids that reflow without breakpoints; the right default for card grids.
|
|
65
|
+
- **[CSS Grid by Example](https://gridbyexample.com/)** — Rachel Andrew's reference; authoritative for advanced grid patterns.
|
|
66
|
+
- **[Radix UI Grid](https://www.radix-ui.com/themes/docs/layout/grid)** — a grid primitive with explicit `columns` prop for column count control.
|
|
67
|
+
|
|
68
|
+
### Minimum quality bar
|
|
69
|
+
|
|
70
|
+
- [ ] Responsive: use `auto-fill` or `auto-fit` with `minmax()` for self-reflowing grids; explicit breakpoints for controlled column counts
|
|
71
|
+
- [ ] Gutters use `gap`, not margin — ensures equal spacing horizontally and vertically
|
|
72
|
+
- [ ] Items in the same row are the same height by default (CSS Grid stretches to match the tallest item)
|
|
73
|
+
- [ ] Named grid areas for complex layouts: makes the layout intent readable in code
|
|
74
|
+
|
|
75
|
+
### Common mistakes
|
|
76
|
+
|
|
77
|
+
- Using `float` or `inline-block` for grid layouts — no longer appropriate; use Grid or Flexbox
|
|
78
|
+
- Fixed-width columns that don't reflow — always pair fixed-width grids with `overflow-x: auto` or responsive breakpoints
|
|
79
|
+
- `auto-fit` vs `auto-fill` confusion: `auto-fit` collapses empty tracks (items stretch); `auto-fill` preserves empty column tracks
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Responsive Patterns
|
|
84
|
+
|
|
85
|
+
Strategies for adapting layout to different viewport widths. Not a component, but a set of decisions that every component must make.
|
|
86
|
+
|
|
87
|
+
### Reference implementations
|
|
88
|
+
|
|
89
|
+
- **[Every Layout](https://every-layout.dev/)** — the complete reference for responsive layout primitives (Stack, Box, Center, Cluster, Sidebar, Switcher, Cover, Grid, Frame, Reel, Imposter, Icon). These patterns cover 95% of layout needs without breakpoints.
|
|
90
|
+
- **[Tailwind responsive prefixes](https://tailwindcss.com/docs/responsive-design)** — `sm:`, `md:`, `lg:`, `xl:` breakpoints for when intrinsic layouts are insufficient.
|
|
91
|
+
|
|
92
|
+
### Minimum quality bar
|
|
93
|
+
|
|
94
|
+
- [ ] Mobile-first: default styles are for small viewports; larger-viewport styles override with `@media (min-width: ...)`
|
|
95
|
+
- [ ] No horizontal scroll on any viewport wider than 320px
|
|
96
|
+
- [ ] Touch targets minimum 44×44px on all interactive elements
|
|
97
|
+
- [ ] Text never smaller than 16px on mobile (prevents browser auto-zoom on iOS)
|
|
98
|
+
- [ ] Images and media use `max-width: 100%` and `height: auto` — never overflow their container
|
|
99
|
+
- [ ] Test at: 320px (smallest iPhone SE), 390px (iPhone 14), 768px (tablet), 1440px (desktop)
|
|
100
|
+
|
|
101
|
+
### Common mistakes
|
|
102
|
+
|
|
103
|
+
- Desktop-first design that patches mobile with `max-width` overrides — creates specificity battles
|
|
104
|
+
- Layout that assumes a specific viewport height — mobile browsers have variable chrome heights
|
|
105
|
+
- `vw` units for font size — creates tiny text on narrow viewports and huge text on wide ones; use `clamp()` for fluid typography
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Sidebar Layout
|
|
110
|
+
|
|
111
|
+
A two-column layout with a persistent sidebar (navigation or contextual content) and a main content area. The most common structure for application dashboards.
|
|
112
|
+
|
|
113
|
+
### Reference implementations
|
|
114
|
+
|
|
115
|
+
- **[shadcn/ui Sidebar](https://ui.shadcn.com/docs/components/sidebar)** — comprehensive implementation: collapsible, icon-only mode, mobile sheet overlay, keyboard navigation, persistent open/closed state.
|
|
116
|
+
- **[Linear's layout](https://linear.app)** — observe: resizable sidebar, persistent state across reloads, collapses to icon-only at narrower widths, slides away on mobile.
|
|
117
|
+
- **[Radix UI layout primitives](https://www.radix-ui.com/themes/docs/layout/box)** — Box and Flex for building custom sidebar layouts.
|
|
118
|
+
|
|
119
|
+
### Minimum quality bar
|
|
120
|
+
|
|
121
|
+
- [ ] Sidebar width is either fixed or resizable within a min/max range
|
|
122
|
+
- [ ] Sidebar open/closed state persists across navigation (local storage or cookie)
|
|
123
|
+
- [ ] Mobile: sidebar slides off-screen; toggle button always visible; opens as overlay, not by pushing content
|
|
124
|
+
- [ ] Main content area uses `min-width: 0` to allow flex children to shrink below content size
|
|
125
|
+
- [ ] Keyboard: pressing a sidebar nav item moves focus into the main content area
|
|
126
|
+
- [ ] Skip-to-content link at page top jumps past the sidebar for keyboard users
|
|
127
|
+
|
|
128
|
+
### Common mistakes
|
|
129
|
+
|
|
130
|
+
- Sidebar with `overflow: hidden` — clips content and breaks dropdown menus that extend beyond the sidebar
|
|
131
|
+
- Sidebar that pushes content off-screen on resize — use CSS variables for width so transitions stay smooth
|
|
132
|
+
- Mobile sidebar that requires a hard page navigation to close — use an overlay and an `×` button
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Cluster / Inline Group
|
|
137
|
+
|
|
138
|
+
A horizontal group of items that wraps to the next line when the container is too narrow. Common for button groups, tag collections, filter chips, and navigation items.
|
|
139
|
+
|
|
140
|
+
### Reference implementations
|
|
141
|
+
|
|
142
|
+
- **[Every Layout — Cluster](https://every-layout.dev/layouts/cluster/)** — `flex-wrap: wrap` with `gap` is the complete pattern; no media queries needed.
|
|
143
|
+
- **[Tailwind flex-wrap](https://tailwindcss.com/docs/flex-wrap)** — `flex flex-wrap gap-2` covers most cluster needs.
|
|
144
|
+
|
|
145
|
+
### Minimum quality bar
|
|
146
|
+
|
|
147
|
+
- [ ] `flex-wrap: wrap` so items reflow naturally rather than overflowing
|
|
148
|
+
- [ ] `gap` (not margin) between items so wrapping rows also have vertical spacing
|
|
149
|
+
- [ ] Alignment: `align-items: center` for mixed-height items (icon + text, badges + text)
|
|
150
|
+
- [ ] When items must not wrap: `overflow-x: auto` with `-webkit-overflow-scrolling: touch` for mobile swipe
|
|
151
|
+
|
|
152
|
+
### Common mistakes
|
|
153
|
+
|
|
154
|
+
- Cluster with margin-right on children but no margin on the last item — creates right overhang
|
|
155
|
+
- Mixing margin and gap for spacing in the same container — pick one
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Navigation — UI References
|
|
2
|
+
|
|
3
|
+
Navigation elements help users understand where they are and move confidently through an application. Poor navigation is one of the top reasons users abandon software. Good navigation is invisible: users find what they need without thinking about the mechanism.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Navigation Bar
|
|
8
|
+
|
|
9
|
+
The persistent top bar that identifies the application and provides access to primary destinations. It must be consistent across all screens and never disappear unexpectedly.
|
|
10
|
+
|
|
11
|
+
### Reference implementations
|
|
12
|
+
|
|
13
|
+
- **[Linear's top nav](https://linear.app)** — observe in the app: no wordmark at scale (icon only), workspace switcher, command bar in center, user avatar at far right. Extremely low visual weight — chrome stays out of the way of content.
|
|
14
|
+
- **[Vercel/Geist Header](https://vercel.com/geist/components)** — persistent nav with responsive collapse to hamburger on mobile; good reference for how a complex nav simplifies at small breakpoints.
|
|
15
|
+
- **[GitHub Primer Global Nav](https://primer.style/components/nav-list/react/alpha)** — handles authenticated vs. unauthenticated states, notification badge, and global search without cluttering the bar.
|
|
16
|
+
- **[Atlassian Navigation](https://atlassian.design/components/atlassian-navigation/examples)** — horizontal nav for complex products with overflow handling when items don't fit.
|
|
17
|
+
|
|
18
|
+
### Minimum quality bar
|
|
19
|
+
|
|
20
|
+
- [ ] `<nav>` element with `aria-label="Main"` (distinguish from secondary navs on the page)
|
|
21
|
+
- [ ] Current page/section has `aria-current="page"` on its link
|
|
22
|
+
- [ ] Keyboard: all links reachable by Tab; no keyboard traps
|
|
23
|
+
- [ ] Mobile: all destinations accessible (not hidden behind a broken hamburger)
|
|
24
|
+
- [ ] Logo links to home; clicking it from home does not reload the page in a disorienting way
|
|
25
|
+
- [ ] Skip navigation link is the first focusable element on the page
|
|
26
|
+
|
|
27
|
+
### Common mistakes
|
|
28
|
+
|
|
29
|
+
- Multiple `<nav role="navigation">` without distinct `aria-label` — screen readers can't distinguish them
|
|
30
|
+
- Hiding the hamburger menu without a visible open state indicator
|
|
31
|
+
- Nav items that look like buttons but navigate — use `<a>`, not `<button>`
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Tabs
|
|
36
|
+
|
|
37
|
+
Tabs switch between views of the same conceptual space. The content area changes; the URL may or may not change depending on whether tabs represent distinct routes.
|
|
38
|
+
|
|
39
|
+
### Reference implementations
|
|
40
|
+
|
|
41
|
+
- **[Radix UI Tabs](https://www.radix-ui.com/primitives/docs/components/tabs)** — the correct `tablist`/`tab`/`tabpanel` ARIA pattern. Arrow key navigation within the tab list, Tab to move focus into the panel. This is the keyboard interaction model to copy.
|
|
42
|
+
- **[shadcn/ui Tabs](https://ui.shadcn.com/docs/components/tabs)** — clean Radix implementation showing horizontal and vertical orientations.
|
|
43
|
+
- **[Linear's sidebar tabs](https://linear.app)** — vertical tabs in a sidebar context; icon+label on wider widths, icon-only on narrower widths with tooltip.
|
|
44
|
+
|
|
45
|
+
### Minimum quality bar
|
|
46
|
+
|
|
47
|
+
- [ ] `role="tablist"` on container, `role="tab"` on each trigger, `role="tabpanel"` on each panel
|
|
48
|
+
- [ ] `aria-selected="true"` on the active tab, `aria-controls` pointing to its panel
|
|
49
|
+
- [ ] Arrow keys (Left/Right for horizontal, Up/Down for vertical) navigate between tabs
|
|
50
|
+
- [ ] Tab key moves focus into the active panel, not to the next tab
|
|
51
|
+
- [ ] Active tab is visually distinct from inactive — not just a color change (underline or elevated background)
|
|
52
|
+
- [ ] If tabs can have counts or badges, they don't overflow the tab trigger width
|
|
53
|
+
|
|
54
|
+
### Common mistakes
|
|
55
|
+
|
|
56
|
+
- Building tabs with buttons and state — without the ARIA roles, screen readers can't identify the pattern
|
|
57
|
+
- Tab key navigating between tabs — breaks the WAI-ARIA tabs keyboard spec
|
|
58
|
+
- Tabs that look like navigation links but don't change the URL — confuse back button behavior
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Sidebar Navigation
|
|
63
|
+
|
|
64
|
+
A persistent vertical list of destinations, usually for primary navigation in dense applications. The sidebar is either always visible (persistent) or toggled (collapsible).
|
|
65
|
+
|
|
66
|
+
### Reference implementations
|
|
67
|
+
|
|
68
|
+
- **[Linear's sidebar](https://linear.app)** — collapsible sections with disclosure triangles, drag-to-reorder items, keyboard-accessible, active item highlighted with subtle background. The state of each section persists across sessions.
|
|
69
|
+
- **[shadcn/ui Sidebar](https://ui.shadcn.com/docs/components/sidebar)** — comprehensive implementation with collapsible, icon-only mode, groups, and responsive mobile sheet behavior.
|
|
70
|
+
- **[Radix UI Navigation Menu](https://www.radix-ui.com/primitives/docs/components/navigation-menu)** — for nested navigation with flyout submenus.
|
|
71
|
+
|
|
72
|
+
### Minimum quality bar
|
|
73
|
+
|
|
74
|
+
- [ ] `<nav>` element with `aria-label` distinct from any other nav
|
|
75
|
+
- [ ] Active item has `aria-current="page"` and a visual highlight (not just color)
|
|
76
|
+
- [ ] Collapsible sections use `<button>` with `aria-expanded`; the trigger is not the section heading itself
|
|
77
|
+
- [ ] Keyboard: arrow keys navigate the tree; Enter expands/collapses; Escape collapses
|
|
78
|
+
- [ ] Mobile: either always visible (if space allows) or a toggleable sheet/drawer — not a hamburger that hides destinations
|
|
79
|
+
|
|
80
|
+
### Common mistakes
|
|
81
|
+
|
|
82
|
+
- Using `<ul>` and `<li>` without navigation semantics — consider `role="navigation"` and `aria-label`
|
|
83
|
+
- Active state using only color — inaccessible; add icon, weight, or background differentiation
|
|
84
|
+
- Sidebar that hides off-screen on mobile but is still Tab-focusable — focus trap for keyboard users
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Breadcrumb
|
|
89
|
+
|
|
90
|
+
Shows the user's location in a hierarchy and lets them jump to any ancestor level. Useful in deeply nested navigation trees; not needed for flat applications.
|
|
91
|
+
|
|
92
|
+
### Reference implementations
|
|
93
|
+
|
|
94
|
+
- **[Radix UI Breadcrumbs](https://www.radix-ui.com/themes/docs/components/breadcrumbs)** — correct `<nav aria-label="Breadcrumb">` wrapper with `<ol>` and `aria-current="page"` on the last item.
|
|
95
|
+
- **[GitHub Primer Breadcrumb](https://primer.style/react/storybook/?path=/story/components-breadcrumbs--default)** — good handling of overflow: ellipsis in the middle, not at the start or end.
|
|
96
|
+
|
|
97
|
+
### Minimum quality bar
|
|
98
|
+
|
|
99
|
+
- [ ] `<nav aria-label="Breadcrumb">` wrapper around an `<ol>`
|
|
100
|
+
- [ ] Last item has `aria-current="page"` and is not a link (it's the current page)
|
|
101
|
+
- [ ] Separator (/) between items is decorative — `aria-hidden="true"` or CSS-generated
|
|
102
|
+
- [ ] On mobile/constrained width: truncate middle items, always show root and current
|
|
103
|
+
- [ ] Schema.org `BreadcrumbList` structured data for SEO when breadcrumbs appear in public pages
|
|
104
|
+
|
|
105
|
+
### Common mistakes
|
|
106
|
+
|
|
107
|
+
- Making the current page item a link — it should not be; it is the current location
|
|
108
|
+
- Separators that are part of the link's tab stop — separators are decorative
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Dropdown Menu
|
|
113
|
+
|
|
114
|
+
A contextual list of actions, exposed from a trigger element. Not a select (select chooses a value); a menu triggers actions.
|
|
115
|
+
|
|
116
|
+
### Reference implementations
|
|
117
|
+
|
|
118
|
+
- **[Radix UI Dropdown Menu](https://www.radix-ui.com/primitives/docs/components/dropdown-menu)** — the complete reference: `menu`, `menuitem`, `menuitemcheckbox`, `menuitemradio` roles, keyboard navigation (arrow keys, Home/End, typeahead), sub-menus, separators.
|
|
119
|
+
- **[shadcn/ui Dropdown Menu](https://ui.shadcn.com/docs/components/dropdown-menu)** — clean implementation with icons, keyboard shortcuts displayed inline, destructive item styling.
|
|
120
|
+
- **[Linear's context menus](https://linear.app)** — observe: keyboard shortcuts shown in the menu (⌘D), dividers grouping related actions, destructive actions at the bottom, subtle hover highlight.
|
|
121
|
+
|
|
122
|
+
### Minimum quality bar
|
|
123
|
+
|
|
124
|
+
- [ ] `role="menu"` on the list, `role="menuitem"` (or `menuitemcheckbox`/`menuitemradio`) on items
|
|
125
|
+
- [ ] Arrow keys navigate; Enter/Space activates; Escape closes and returns focus to trigger
|
|
126
|
+
- [ ] Typeahead: pressing a letter jumps to first item starting with that letter
|
|
127
|
+
- [ ] Nested submenus open on arrow-right; close on arrow-left or Escape
|
|
128
|
+
- [ ] Destructive items visually distinct (red label); positioned last in the group
|
|
129
|
+
- [ ] Menu closes when user clicks outside or navigates away; focus returns to trigger
|
|
130
|
+
|
|
131
|
+
### Common mistakes
|
|
132
|
+
|
|
133
|
+
- Using a dropdown menu for a select — `role="menu"` does not communicate value selection to screen readers
|
|
134
|
+
- Not returning focus to the trigger when the menu closes
|
|
135
|
+
- Sub-menus that open on hover (not arrow key) — inaccessible keyboard pattern
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Pagination
|
|
140
|
+
|
|
141
|
+
Controls for navigating through paged data. Users need to know their current position, total pages (if known), and where to go next.
|
|
142
|
+
|
|
143
|
+
### Reference implementations
|
|
144
|
+
|
|
145
|
+
- **[shadcn/ui Pagination](https://ui.shadcn.com/docs/components/pagination)** — previous/next arrows with page number range in between; ellipsis for large ranges.
|
|
146
|
+
- **[GitHub Primer Pagination](https://primer.style/components/pagination)** — handles `aria-label` per page link ("Page 2"), `aria-current="page"` on the active page, previous/next with icon + text.
|
|
147
|
+
|
|
148
|
+
### Minimum quality bar
|
|
149
|
+
|
|
150
|
+
- [ ] `<nav aria-label="Pagination">` wrapping the controls
|
|
151
|
+
- [ ] Each page link has a unique, descriptive `aria-label` ("Page 3")
|
|
152
|
+
- [ ] Current page has `aria-current="page"` and is not a link
|
|
153
|
+
- [ ] Previous/Next buttons are disabled (not hidden) on the first/last page; `aria-disabled="true"` and `disabled`
|
|
154
|
+
- [ ] Ellipsis items are not interactive and are `aria-hidden="true"`
|
|
155
|
+
- [ ] Keyboard: Tab reaches each page link; Enter activates
|
|
156
|
+
|
|
157
|
+
### Common mistakes
|
|
158
|
+
|
|
159
|
+
- Hiding previous on page 1 and next on last page — users lose spatial context about the range
|
|
160
|
+
- Ellipsis items that are also links — they should not be clickable
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Command Palette
|
|
165
|
+
|
|
166
|
+
A keyboard-first search interface for navigating and acting across the entire application. Found in Linear, VSCode, GitHub, Vercel. The power-user's primary navigation.
|
|
167
|
+
|
|
168
|
+
### Reference implementations
|
|
169
|
+
|
|
170
|
+
- **[cmdk](https://cmdk.paco.me/)** — the canonical open-source command palette component; accessible, keyboard-driven, fuzzy search, used by shadcn/ui and Linear.
|
|
171
|
+
- **[Linear's command palette](https://linear.app)** — press `⌘K`; observe: fuzzy search, grouping, keyboard shortcuts shown, recent items at top, no mouse required after open.
|
|
172
|
+
- **[shadcn/ui Command](https://ui.shadcn.com/docs/components/command)** — cmdk with Tailwind styling; includes the modal wrapper pattern.
|
|
173
|
+
|
|
174
|
+
### Minimum quality bar
|
|
175
|
+
|
|
176
|
+
- [ ] Opens with `⌘K` (Mac) / `Ctrl+K` (Windows/Linux); closes with Escape
|
|
177
|
+
- [ ] Focus is trapped inside while open; returns to previous element on close
|
|
178
|
+
- [ ] Search input is focused immediately on open
|
|
179
|
+
- [ ] Keyboard: arrow keys navigate items; Enter activates; Tab should also navigate
|
|
180
|
+
- [ ] Items are grouped (e.g. "Navigation", "Actions", "Recent")
|
|
181
|
+
- [ ] Empty state is shown when no matches (not a blank list)
|
|
182
|
+
- [ ] `role="combobox"` on the input, `role="listbox"` on the list, `role="option"` on items
|
|
183
|
+
|
|
184
|
+
### Common mistakes
|
|
185
|
+
|
|
186
|
+
- Requiring mouse to open — defeats the purpose of a keyboard-first tool
|
|
187
|
+
- No empty state — users don't know if the search is broken or there are no results
|
|
188
|
+
- Slow filtering — command palettes must filter on every keystroke instantly (<16ms)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Surface — UI References
|
|
2
|
+
|
|
3
|
+
Surface components are containers and overlays: they frame content, present it in context, and layer it above other content. The quality of a surface shows in its edge cases — what happens when content overflows, when it's positioned near a viewport edge, or when a screen reader announces it.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Card
|
|
8
|
+
|
|
9
|
+
A bounded content container that groups related information. Cards are surfaces, not actions: they can be clickable (linking to detail views) but clicking a card is navigation, not a form submission.
|
|
10
|
+
|
|
11
|
+
### Reference implementations
|
|
12
|
+
|
|
13
|
+
- **[shadcn/ui Card](https://ui.shadcn.com/docs/components/card)** — `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` composition pattern; clear separation of structural slots.
|
|
14
|
+
- **[Radix UI Card](https://www.radix-ui.com/themes/docs/components/card)** — minimal themed card with consistent shadow and radius; good baseline for a design system.
|
|
15
|
+
- **[Linear's cards](https://linear.app)** — observe in issues and project views: no excessive shadow, border defines the edge, hover state is a background tint (not elevation change), active selection uses a strong left border.
|
|
16
|
+
- **[Vercel/Geist Card](https://vercel.com/geist/components)** — flat cards with border; the standard for enterprise/dashboard contexts.
|
|
17
|
+
|
|
18
|
+
### Minimum quality bar
|
|
19
|
+
|
|
20
|
+
- [ ] Consistent internal padding (match the project's grid unit)
|
|
21
|
+
- [ ] Container border-radius matches the project's `container` radius value — not the `control` radius
|
|
22
|
+
- [ ] If the whole card is clickable: `<a>` wrapping the card (not an `onClick` on a `<div>`)
|
|
23
|
+
- [ ] If part of the card is clickable (e.g. title link + action button): use `::after` pseudo-element on the title link to expand its hit area to the card; action button has its own event without conflict
|
|
24
|
+
- [ ] Hover state is visible; active/pressed state is visible
|
|
25
|
+
- [ ] Card body does not use `overflow: hidden` aggressively — truncation should be controlled and indicated
|
|
26
|
+
|
|
27
|
+
### Common mistakes
|
|
28
|
+
|
|
29
|
+
- Wrapping an entire card in `<a>` when it also contains buttons — creates nested interactive elements and confuses screen readers
|
|
30
|
+
- Elevation (box-shadow) as the primary border — elevation should supplement, not replace, a visible edge
|
|
31
|
+
- Fixed height cards that hide overflow without indication — use truncation with ellipsis and tooltip, not silent clipping
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Popover
|
|
36
|
+
|
|
37
|
+
A floating overlay anchored to a trigger element. Used for secondary actions, rich tooltips, or inline editing. Differs from a tooltip: popovers can contain interactive elements.
|
|
38
|
+
|
|
39
|
+
### Reference implementations
|
|
40
|
+
|
|
41
|
+
- **[Radix UI Popover](https://www.radix-ui.com/primitives/docs/components/popover)** — the reference: `role="dialog"` when interactive, focus moves into the popover on open, Escape closes and returns focus. `side`, `align`, and `avoidCollisions` handle positioning near viewport edges.
|
|
42
|
+
- **[shadcn/ui Popover](https://ui.shadcn.com/docs/components/popover)** — Radix Popover with Tailwind; shows the trigger + content pattern cleanly.
|
|
43
|
+
- **[Floating UI](https://floating-ui.com/)** — the positioning library underlying Radix, Headless UI, and others; consult for edge cases in custom positioning logic.
|
|
44
|
+
|
|
45
|
+
### Minimum quality bar
|
|
46
|
+
|
|
47
|
+
- [ ] `role="dialog"` with `aria-label` or `aria-labelledby` when the popover contains interactive content
|
|
48
|
+
- [ ] Focus moves into the popover when it opens (for interactive popovers)
|
|
49
|
+
- [ ] Escape closes the popover; click outside closes; focus returns to trigger
|
|
50
|
+
- [ ] Stays within viewport: flips side and/or shifts along axis when near an edge
|
|
51
|
+
- [ ] Arrow indicator (optional but helpful) points to the anchor and repositions when the popover flips
|
|
52
|
+
- [ ] Does not scroll the page body when scrolling inside the popover
|
|
53
|
+
|
|
54
|
+
### Common mistakes
|
|
55
|
+
|
|
56
|
+
- Non-interactive popovers that receive focus — only interactive popovers should trap/move focus
|
|
57
|
+
- Popover that overflows the viewport on small screens — always test positioning at mobile widths
|
|
58
|
+
- Opening a popover on hover without a click fallback — hover-only is inaccessible
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Tooltip
|
|
63
|
+
|
|
64
|
+
A small informational overlay that appears on hover (and focus). Tooltips explain UI elements; they do not contain interactive elements. If you need interactive content in the overlay, use a Popover.
|
|
65
|
+
|
|
66
|
+
### Reference implementations
|
|
67
|
+
|
|
68
|
+
- **[Radix UI Tooltip](https://www.radix-ui.com/primitives/docs/components/tooltip)** — `role="tooltip"` on the content, `aria-describedby` linking the trigger to the tooltip. Delay before showing (300ms default), immediate hide on mouse leave.
|
|
69
|
+
- **[shadcn/ui Tooltip](https://ui.shadcn.com/docs/components/tooltip)** — clean implementation; the `TooltipProvider` wrapping pattern for shared delay configuration.
|
|
70
|
+
- **[Floating UI Tooltip tutorial](https://floating-ui.com/docs/tooltip)** — explains hover + focus event handling without ARIA mistakes.
|
|
71
|
+
|
|
72
|
+
### Minimum quality bar
|
|
73
|
+
|
|
74
|
+
- [ ] `role="tooltip"` on the content element
|
|
75
|
+
- [ ] Trigger has `aria-describedby` pointing to the tooltip id
|
|
76
|
+
- [ ] Shows on hover AND on keyboard focus (not hover only)
|
|
77
|
+
- [ ] Delay before showing (200–300ms) prevents tooltip flicker on cursor pass-through
|
|
78
|
+
- [ ] Tooltip is dismissible via Escape key while visible
|
|
79
|
+
- [ ] Does not obscure the element being described
|
|
80
|
+
- [ ] Text content only — if you need a button inside, it's a Popover
|
|
81
|
+
- [ ] Respects `prefers-reduced-motion` for entry animation
|
|
82
|
+
|
|
83
|
+
### Common mistakes
|
|
84
|
+
|
|
85
|
+
- Tooltip on a disabled element — disabled elements don't receive focus events; wrap the disabled element in a `<span>` for the tooltip
|
|
86
|
+
- Tooltip that duplicates the visible label exactly — adds no value; tooltip text should supplement, not repeat
|
|
87
|
+
- Interactive content (links, buttons) inside a tooltip — use Popover instead
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Sheet / Drawer
|
|
92
|
+
|
|
93
|
+
A panel that slides in from an edge of the screen (most commonly the right). Used for secondary task panels, filter drawers, detail views, or mobile navigation.
|
|
94
|
+
|
|
95
|
+
### Reference implementations
|
|
96
|
+
|
|
97
|
+
- **[shadcn/ui Sheet](https://ui.shadcn.com/docs/components/sheet)** — four opening directions (top, right, bottom, left); `role="dialog"`, focus trap, Escape to close; uses the Radix UI Dialog primitive underneath.
|
|
98
|
+
- **[Radix UI Dialog as Sheet](https://www.radix-ui.com/primitives/docs/components/dialog)** — sheets are typically implemented on top of a dialog primitive with CSS positioning.
|
|
99
|
+
- **[Linear's right panel](https://linear.app)** — observe the issue detail panel: slides in from the right, does not cover the main content at wider viewports (side-by-side layout), full-screen on mobile.
|
|
100
|
+
|
|
101
|
+
### Minimum quality bar
|
|
102
|
+
|
|
103
|
+
- [ ] `role="dialog"`, `aria-modal="true"`, `aria-label` or `aria-labelledby`
|
|
104
|
+
- [ ] Focus trap: Tab/Shift+Tab cycle inside the sheet while open
|
|
105
|
+
- [ ] Opens with slide animation; closes with slide animation; `prefers-reduced-motion` respects instant open/close
|
|
106
|
+
- [ ] Escape closes; clicking the scrim closes; close button always visible
|
|
107
|
+
- [ ] On mobile: always full-screen or nearly full-screen — partial drawers cause content-below-fold confusion
|
|
108
|
+
- [ ] Handles soft keyboard on mobile (input fields inside the sheet must be visible when keyboard opens)
|
|
109
|
+
|
|
110
|
+
### Common mistakes
|
|
111
|
+
|
|
112
|
+
- Sheet that cannot be closed without completing a form — always allow escape/scrim close
|
|
113
|
+
- Sheet that scrolls the background page — lock body scroll while sheet is open
|
|
114
|
+
- Nested sheets — avoid; create a single sheet that can change its contents instead
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Avatar
|
|
119
|
+
|
|
120
|
+
Represents a user or entity with an image, initials, or icon. Appears in lists, comments, headers, and presence indicators.
|
|
121
|
+
|
|
122
|
+
### Reference implementations
|
|
123
|
+
|
|
124
|
+
- **[Radix UI Avatar](https://www.radix-ui.com/primitives/docs/components/avatar)** — image with `AvatarFallback` that shows while image loads or on error. The fallback timing (delay before fallback renders) avoids flash-of-initials before the image loads.
|
|
125
|
+
- **[shadcn/ui Avatar](https://ui.shadcn.com/docs/components/avatar)** — clean implementation; the image + fallback pattern with consistent sizing.
|
|
126
|
+
|
|
127
|
+
### Minimum quality bar
|
|
128
|
+
|
|
129
|
+
- [ ] Image has `alt` text describing who it represents (not `alt=""` — that's for decorative images)
|
|
130
|
+
- [ ] Fallback (initials or generic icon) when image fails to load or is not provided
|
|
131
|
+
- [ ] Fallback renders deterministically — same initials/color every time for the same user
|
|
132
|
+
- [ ] Consistent sizes across the product — define a size scale (sm/md/lg) and stick to it
|
|
133
|
+
- [ ] Avatar groups (stacked avatars): `z-index` and margin-left negative offset done with CSS, not absolute positioning
|
|
134
|
+
|
|
135
|
+
### Common mistakes
|
|
136
|
+
|
|
137
|
+
- `alt=""` on user avatar images — these are meaningful, not decorative; describe who the image shows
|
|
138
|
+
- Avatar that shows a broken image icon instead of fallback — always handle image load failure
|
|
139
|
+
- Fallback background color that is too light against a white background — ensure sufficient contrast
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Separator / Divider
|
|
144
|
+
|
|
145
|
+
A visual line that separates groups of content. Structural, not decorative — it communicates that what's above and below belong to different groups.
|
|
146
|
+
|
|
147
|
+
### Reference implementations
|
|
148
|
+
|
|
149
|
+
- **[Radix UI Separator](https://www.radix-ui.com/primitives/docs/components/separator)** — `role="separator"` (structural) or `role="none"` (decorative), horizontal and vertical orientation.
|
|
150
|
+
- **[shadcn/ui Separator](https://ui.shadcn.com/docs/components/separator)** — thin horizontal/vertical rule.
|
|
151
|
+
|
|
152
|
+
### Minimum quality bar
|
|
153
|
+
|
|
154
|
+
- [ ] `role="separator"` when the line communicates structure (between sections); `role="none"` or `aria-hidden="true"` when purely decorative
|
|
155
|
+
- [ ] Color passes 3:1 contrast against its background
|
|
156
|
+
- [ ] Does not carry margin by default — let the container control spacing
|
|
157
|
+
- [ ] Horizontal separators extend to the full width of their container unless explicitly inset
|
|
158
|
+
|
|
159
|
+
### Common mistakes
|
|
160
|
+
|
|
161
|
+
- Using `<hr>` inside inline contexts — `<hr>` has block display; use `role="separator"` on a `<span>` for inline separators
|
|
162
|
+
- Separator with too much vertical margin — creates too much visual space between related groups
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Badge
|
|
167
|
+
|
|
168
|
+
A small label applied to an element to communicate status, count, or category. Appears inline next to text or as an overlay on an icon.
|
|
169
|
+
|
|
170
|
+
### Reference implementations
|
|
171
|
+
|
|
172
|
+
- **[shadcn/ui Badge](https://ui.shadcn.com/docs/components/badge)** — variant system (default, secondary, destructive, outline); small, not button-like.
|
|
173
|
+
- **[Radix UI Badge](https://www.radix-ui.com/themes/docs/components/badge)** — color-coded badges with good contrast defaults across themes.
|
|
174
|
+
- **[Linear's labels](https://linear.app)** — observe issue labels: colored dot + text, keyboard accessible in filter panels, consistent small size.
|
|
175
|
+
- **[GitHub Primer Label](https://primer.style/components/label/react/alpha)** — handles "filled" and "outline" variants with color theming.
|
|
176
|
+
|
|
177
|
+
### Minimum quality bar
|
|
178
|
+
|
|
179
|
+
- [ ] Not a button — badges are not interactive; if clicking a badge does something, it should be a `<button>` or `<a>` visually styled as a badge
|
|
180
|
+
- [ ] Text must pass 4.5:1 contrast against its background color
|
|
181
|
+
- [ ] Numeric badges (notification counts): `aria-label="N notifications"` on the parent container; the badge itself is `aria-hidden`
|
|
182
|
+
- [ ] Never convey information through color alone — include text or icon alongside color coding
|
|
183
|
+
- [ ] Consistent height and padding across all badge uses in the product
|
|
184
|
+
|
|
185
|
+
### Common mistakes
|
|
186
|
+
|
|
187
|
+
- Badge that is a `<div>` with `onClick` — should be `<button>` for interactivity or `<span>` for decoration
|
|
188
|
+
- Too many badge variants — three variants (default, success, danger) cover most cases; more adds cognitive load
|