whale-igniter 1.3.0 → 1.3.2

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.
@@ -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)