# Product UI Patterns — Linear-style components, in code > A practical deep-dive into the components that make Linear-style product UI feel considered. Each pattern includes concrete HTML + CSS, all states, and what to avoid. Use this when building product chrome (sidebar, command palette, lists, modals) — not for marketing pages. --- ## How to use this file This file is **code-first**. When you need to build a sidebar, command palette, segmented control, or status indicator for a Linear-style product, come here. Each component has: 1. **When to use it** 2. **Anatomy** (what it's made of) 3. **HTML** (semantic markup) 4. **CSS** (with tokens, all states) 5. **States covered** (default, hover, focus, active, disabled, loading) 6. **Anti-patterns** (what breaks the pattern) All CSS assumes the Linear-style dark token system (see `minimal-ui-patterns.md` §1 for tokens). Adapt colors to your palette. --- ## 1. Sidebar (navigation) **Used in:** Linear, Notion, Figma, most SaaS apps. ### Anatomy - Container with hairline right border - Section labels (in caps mono, wide tracking) - List of nav items (icon + label) - Active state (background tint or left accent border) - Hover state (subtle background tint) - Footer (user avatar, workspace switcher) ### HTML ```html ``` ### CSS ```css .sidebar { width: 240px; height: 100vh; background: var(--surface-1); border-right: 1px solid var(--hairline); display: flex; flex-direction: column; position: sticky; top: 0; } .sidebar__inner { display: flex; flex-direction: column; height: 100%; padding: var(--sp-3); } .sidebar__head { padding: var(--sp-2) var(--sp-3); margin-bottom: var(--sp-3); } .workspace-switch { display: flex; align-items: center; gap: var(--sp-2); width: 100%; padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm); font-size: var(--text-sm); font-weight: 500; color: var(--ink); transition: background 120ms ease; } .workspace-switch:hover { background: var(--surface-2); } .workspace-switch:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } .workspace-mark { color: var(--accent); } .workspace-name { flex: 1; text-align: left; } .workspace-chevron { color: var(--ink-muted); font-size: 11px; } .sidebar__nav { flex: 1; overflow-y: auto; } .sidebar__section { margin-bottom: var(--sp-5); } .sidebar__label { font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-subtle); padding: 0 var(--sp-3); margin-bottom: var(--sp-2); font-weight: 500; } .sidebar__section ul { list-style: none; margin: 0; padding: 0; } .sidebar__item { display: flex; align-items: center; gap: var(--sp-3); padding: 6px var(--sp-3); border-radius: var(--r-sm); font-size: var(--text-sm); color: var(--ink-muted); text-decoration: none; transition: background 100ms ease, color 100ms ease; } .sidebar__item:hover { background: var(--surface-2); color: var(--ink); } .sidebar__item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } .sidebar__item--active { background: var(--accent-soft); color: var(--ink); font-weight: 500; } .sidebar__icon { color: currentColor; flex-shrink: 0; } .sidebar__count { margin-left: auto; font-family: var(--font-mono); font-size: 11px; padding: 1px 6px; border-radius: 3px; background: var(--surface-2); color: var(--ink-muted); } .sidebar__team-mark { font-size: 11px; color: var(--ink-muted); width: 16px; text-align: center; } .sidebar__foot { border-top: 1px solid var(--hairline); padding-top: var(--sp-3); margin-top: var(--sp-3); } .user-menu { display: flex; align-items: center; gap: var(--sp-2); width: 100%; padding: var(--sp-2) var(--sp-3); border-radius: var(--r-sm); font-size: var(--text-sm); color: var(--ink); transition: background 120ms ease; } .user-menu:hover { background: var(--surface-2); } .avatar { width: 20px; height: 20px; border-radius: 50%; background: var(--accent); color: var(--surface); font-size: 10px; font-weight: 600; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; } .user-menu__name { flex: 1; text-align: left; } ``` ### Anti-patterns - ❌ Sidebar > 280px wide (consumes too much horizontal space) - ❌ Active state as bold text only (no background or border) - ❌ Icon-only nav without tooltips - ❌ Section labels in regular case (must be mono caps with tracking) - ❌ Hover state that shifts layout (causes jank) --- ## 2. Command palette (⌘K) **Used in:** Linear, GitHub, Vercel, Raycast, every modern SaaS. ### Anatomy - Centered modal, max-width 560–640px - Input field at top, large, no border - List of results below, keyboard-driven - Keyboard shortcuts visible on right side of each row - Categories separated by hairline + section label - Empty state for "no results" ### HTML ```html ``` ### CSS ```css .command-palette { position: fixed; inset: 0; z-index: 100; display: flex; align-items: flex-start; justify-content: center; padding-top: 12vh; } .command-palette__backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(2px); } .command-palette__panel { position: relative; width: 100%; max-width: 600px; background: var(--surface-1); border: 1px solid var(--hairline-strong); border-radius: var(--r-lg); box-shadow: 0 24px 48px rgba(0, 0, 0, 0.5); overflow: hidden; animation: cp-in 140ms ease-out; } @keyframes cp-in { from { opacity: 0; transform: scale(0.98) translateY(-8px); } to { opacity: 1; transform: scale(1) translateY(0); } } .cp-input-wrap { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--hairline); } .cp-search-icon { color: var(--ink-muted); flex-shrink: 0; } .cp-input { flex: 1; font-size: var(--text-md); color: var(--ink); background: transparent; border: 0; outline: 0; } .cp-input::placeholder { color: var(--ink-subtle); } .cp-esc { font-family: var(--font-mono); font-size: 11px; padding: 2px 6px; border-radius: 3px; background: var(--surface-2); border: 1px solid var(--hairline); color: var(--ink-muted); } .cp-results { max-height: 60vh; overflow-y: auto; padding: var(--sp-3); } .cp-group { margin-bottom: var(--sp-3); } .cp-group:last-child { margin-bottom: 0; } .cp-group__label { font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-subtle); padding: var(--sp-2) var(--sp-3); margin: 0; font-weight: 500; } .cp-group ul { list-style: none; margin: 0; padding: 0; } .cp-result { display: flex; align-items: center; gap: var(--sp-3); padding: var(--sp-3); border-radius: var(--r-sm); cursor: pointer; font-size: var(--text-sm); transition: background 80ms ease; } .cp-result:hover { background: var(--surface-2); } .cp-result--selected { background: var(--accent-soft); } .cp-result--selected .cp-result__title { color: var(--accent); } .cp-result__icon { width: 20px; height: 20px; border-radius: 3px; background: var(--surface-2); display: inline-flex; align-items: center; justify-content: center; font-size: 12px; color: var(--ink-muted); flex-shrink: 0; } .cp-result__main { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; } .cp-result__title { color: var(--ink); font-weight: 500; } .cp-result__sub { color: var(--ink-muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .cp-result__meta { font-family: var(--font-mono); font-size: 11px; color: var(--ink-subtle); } .cp-result__kbd { font-family: var(--font-mono); font-size: 11px; padding: 2px 6px; border-radius: 3px; background: var(--surface-2); border: 1px solid var(--hairline); color: var(--ink-muted); } .cp-foot { display: flex; gap: var(--sp-5); padding: var(--sp-3) var(--sp-5); border-top: 1px solid var(--hairline); font-size: 11px; color: var(--ink-subtle); } .cp-foot kbd { font-family: var(--font-mono); font-size: 10px; padding: 1px 5px; border-radius: 3px; background: var(--surface-2); border: 1px solid var(--hairline); margin-right: 4px; } ``` ### Behavior (JS sketch) ```js // Open on ⌘K / Ctrl+K // Close on Esc or click outside // Up/Down to navigate // Enter to select // Auto-focus input on open // Reset query on close ``` ### Anti-patterns - ❌ Backdrop too dark (0.7+) — feels oppressive - ❌ Animation > 200ms (feels slow for power-user feature) - ❌ No keyboard navigation (this is a keyboard-first feature) - ❌ Hidden close button (must be discoverable) - ❌ Results without categories (hard to scan) - ❌ No "no results" empty state --- ## 3. Empty states **Used in:** Every list view, dashboard, settings page. ### Anatomy - Centered or left-aligned container - Single illustration or geometric icon (subtle, restrained) - One headline explaining what should be here - One sentence of context (optional) - One primary action button - Optional secondary link (e.g., "Read docs") ### HTML ```html

No issues yet

Issues track the work your team needs to do. Create your first one to get started.

Read the docs →
``` ### CSS ```css .empty-state { display: flex; flex-direction: column; align-items: center; text-align: center; padding: var(--sp-9) var(--sp-5); max-width: 400px; margin: 0 auto; } .empty-state__icon { color: var(--ink-subtle); margin-bottom: var(--sp-5); } .empty-state__title { font-size: var(--text-lg); font-weight: 600; color: var(--ink); margin: 0 0 var(--sp-2) 0; letter-spacing: -0.01em; } .empty-state__desc { font-size: var(--text-sm); color: var(--ink-muted); line-height: 1.5; margin: 0 0 var(--sp-5) 0; } .empty-state__actions { display: flex; align-items: center; gap: var(--sp-4); } .empty-state__link { font-size: var(--text-sm); color: var(--ink-muted); text-decoration: none; border-bottom: 1px solid var(--hairline); padding-bottom: 1px; transition: color 120ms ease, border-color 120ms ease; } .empty-state__link:hover { color: var(--accent); border-color: var(--accent); } ``` ### Anti-patterns - ❌ Stock illustration of confused person - ❌ Sad emoji - ❌ Two action buttons competing - ❌ Empty state that says "No data" (meaningless) - ❌ Centered illustration that's huge (200px+) — feels desperate --- ## 4. List items **Used in:** Inbox, tables-of-contents, search results, settings lists. ### Anatomy - 40–48px row height - Single line of primary content - Optional secondary line (subdued) - Left: icon or status indicator - Right: metadata or actions - Hairline divider between rows - Hover: subtle background tint - Selected: accent-tinted background ### HTML ```html ``` ### CSS ```css .list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--hairline); } .list__item { border-bottom: 1px solid var(--hairline); } .list__link { display: grid; grid-template-columns: 12px minmax(0, 1fr) auto auto auto; gap: var(--sp-3); align-items: center; padding: var(--sp-3) var(--sp-4); font-size: var(--text-sm); color: var(--ink); text-decoration: none; transition: background 100ms ease; } .list__link:hover { background: var(--surface-2); } .list__link:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } .list__status { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } .list__status--todo { background: var(--ink-subtle); } .list__status--in-progress { background: var(--warn); } .list__status--done { background: var(--good); } .list__status--blocked { background: var(--bad); } .list__main { display: flex; align-items: baseline; gap: var(--sp-3); min-width: 0; } .list__id { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-subtle); flex-shrink: 0; } .list__title { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .list__priority { color: var(--warn); display: inline-flex; } .list__assignee { display: inline-flex; } .avatar--xs { width: 18px; height: 18px; font-size: 9px; } .list__date { font-family: var(--font-mono); font-size: var(--text-xs); color: var(--ink-subtle); white-space: nowrap; } ``` ### Anti-patterns - ❌ Row height > 56px (consumes too much space) - ❌ Multiple status indicators stacked - ❌ Right-side metadata that wraps - ❌ Hover that shifts content (causes jank) - ❌ Border between every row + border-radius (conflicting) --- ## 5. Status indicators (badges, dots, pills) **Used in:** Issue trackers, project management, notifications. ### Anatomy — three types **Dot** (8px circle, semantic color) - Inbox counts, status indicators, presence - One color, no text **Badge** (rounded rectangle with text) - Status labels ("In review", "Blocked") - Small (11–12px text), padded **Pill** (full-rounded rectangle with text) - Tags, categories - Slightly larger than badge ### HTML + CSS ```html In review Engineering ``` ```css /* Dot */ .status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } .status-dot--todo { background: var(--ink-subtle); } .status-dot--in-progress { background: var(--warn); } .status-dot--done { background: var(--good); } .status-dot--blocked { background: var(--bad); } /* Badge */ .status-badge { display: inline-flex; align-items: center; height: 20px; padding: 0 8px; border-radius: var(--r-sm); font-size: 11px; font-weight: 500; letter-spacing: 0; white-space: nowrap; } .status-badge--todo { background: var(--surface-2); color: var(--ink-muted); } .status-badge--in-progress { background: rgba(251, 191, 36, 0.15); color: var(--warn); } .status-badge--review { background: rgba(99, 91, 255, 0.15); color: var(--accent); } .status-badge--done { background: rgba(76, 183, 130, 0.15); color: var(--good); } .status-badge--blocked { background: rgba(248, 113, 113, 0.15); color: var(--bad); } /* Pill / tag */ .tag { display: inline-flex; align-items: center; height: 22px; padding: 0 10px; border-radius: 9999px; /* pill is OK for tags — they're meant to be pills */ font-size: 12px; font-weight: 500; background: var(--surface-2); color: var(--ink-muted); border: 1px solid var(--hairline); } ``` ### Anti-patterns - ❌ Using pill on every interactive element (only for tags/categories) - ❌ Too many color states (stick to 3–4 semantic colors) - ❌ Badges with no text (just a dot) — ambiguous - ❌ Text inside badges too long (truncate or shorten label) --- ## 6. Segmented control **Used in:** Time range selectors (1h / 24h / 7d / 30d), view toggles. ### Anatomy - Container with hairline border, subtle background - Buttons inside, equal-width or content-width - Active button: surface background, slight shadow - Hover: ink color change - Mono font for time labels (technical feel) ### HTML ```html
``` ### CSS ```css .segmented { display: inline-flex; background: var(--surface-2); border: 1px solid var(--hairline); border-radius: var(--r-sm); padding: 2px; gap: 0; } .segmented__btn { padding: 4px 10px; font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.04em; color: var(--ink-muted); border-radius: 3px; transition: color 120ms ease, background 120ms ease; white-space: nowrap; } .segmented__btn:hover { color: var(--ink); } .segmented__btn--active { background: var(--surface); color: var(--ink); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } .segmented__btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } ``` ### Anti-patterns - ❌ Active state as bold text only (no background change) - ❌ Buttons without aria-selected (a11y failure) - ❌ Sans font for time labels (mono reads as more precise) - ❌ Animated transitions on click (sluggish for power users) --- ## 7. Tables (data tables) **Used in:** Settings, admin panels, lists with structured data. ### Anatomy - Header row: caps mono, muted, smaller text - Body rows: 48px height, hairline bottom border - Numbers: mono font, tabular figures, right-aligned - Text columns: left-aligned - Hover row: subtle background tint - Action column: right-aligned, icon buttons or text links ### HTML ```html
Name Role Last active Sessions
Jane Doe Admin 2 hours ago 1,247
``` ### CSS ```css .data-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); } .data-table thead th { text-align: left; font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-muted); font-weight: 500; padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--hairline); } .data-table tbody td { padding: var(--sp-4); border-bottom: 1px solid var(--hairline); color: var(--ink); vertical-align: middle; } .data-table tbody tr { transition: background 80ms ease; } .data-table tbody tr:hover { background: var(--surface-2); } .data-table__num { text-align: right; font-family: var(--font-mono); font-feature-settings: 'tnum' 1; } .data-table__actions { text-align: right; width: 40px; } .icon-btn { width: 28px; height: 28px; border-radius: var(--r-sm); color: var(--ink-muted); display: inline-flex; align-items: center; justify-content: center; transition: background 120ms ease, color 120ms ease; } .icon-btn:hover { background: var(--surface-2); color: var(--ink); } .icon-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } ``` ### Anti-patterns - ❌ Full grid of borders (looks like Excel) - ❌ Centered data cells (left-align text, right-align numbers) - ❌ Wrapping headers (use shorter labels) - ❌ Inconsistent row heights - ❌ Sortable columns without visual indicator --- ## 8. Modal / dialog **Used in:** Create flows, confirmations, settings panels. ### Anatomy - Centered, max-width 480–560px (forms), 720px+ (content) - Backdrop: rgba(0,0,0, 0.4–0.6) - Surface: elevated panel - Border-radius: 8–12px - Padding: 24–32px - Close: X button (top-right) + Esc key - Focus trap (keyboard focus stays inside) - Animation: fade + slight scale (0.98 → 1), 150ms ### HTML ```html ``` ### CSS ```css .modal { position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; padding: var(--sp-5); } .modal__backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.5); animation: modal-backdrop-in 150ms ease-out; } @keyframes modal-backdrop-in { from { opacity: 0; } to { opacity: 1; } } .modal__panel { position: relative; width: 100%; max-width: 520px; background: var(--surface-1); border: 1px solid var(--hairline-strong); border-radius: var(--r-lg); box-shadow: 0 24px 48px rgba(0, 0, 0, 0.5); display: flex; flex-direction: column; max-height: 90vh; animation: modal-panel-in 180ms ease-out; } @keyframes modal-panel-in { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } } .modal__head { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-5) var(--sp-6); border-bottom: 1px solid var(--hairline); } .modal__title { font-size: var(--text-lg); font-weight: 600; color: var(--ink); margin: 0; letter-spacing: -0.01em; } .modal__close { width: 28px; height: 28px; border-radius: var(--r-sm); color: var(--ink-muted); display: inline-flex; align-items: center; justify-content: center; transition: background 120ms ease, color 120ms ease; } .modal__close:hover { background: var(--surface-2); color: var(--ink); } .modal__close:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; } .modal__body { padding: var(--sp-6); overflow-y: auto; flex: 1; } .modal__foot { display: flex; justify-content: flex-end; gap: var(--sp-3); padding: var(--sp-4) var(--sp-6); border-top: 1px solid var(--hairline); } ``` ### Anti-patterns - ❌ Backdrop > 0.6 opacity (oppressive) - ❌ Animation > 250ms (feels slow) - ❌ No Esc key handler - ❌ No focus trap (Tab escapes modal) - ❌ Two competing primary buttons in footer - ❌ Modal that can't scroll on small screens --- ## 9. Form inputs (Linear-style) **Used in:** All forms, settings, search. ### Anatomy - 36–40px height - 1px hairline-strong border, surface-sunken background - Border-radius: 6px - Focus: border becomes accent, 2px focus ring with offset - Label above input, mono caps, small - Help text below, smaller, muted ### HTML ```html

Use a verb-led sentence. "Fix checkout error" not "Checkout error".

``` ### CSS ```css .field { display: flex; flex-direction: column; gap: var(--sp-2); margin-bottom: var(--sp-5); } .field__label { font-family: var(--font-mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-muted); font-weight: 500; } .field__input, .field__select, .field__textarea { width: 100%; height: 36px; padding: 0 var(--sp-3); background: var(--surface-sunken); border: 1px solid var(--hairline-strong); border-radius: var(--r-md); font-size: var(--text-sm); color: var(--ink); transition: border-color 120ms ease, background 120ms ease; } .field__input::placeholder { color: var(--ink-subtle); } .field__input:hover, .field__select:hover { border-color: var(--ink-muted); } .field__input:focus, .field__select:focus { border-color: var(--accent); outline: 2px solid var(--accent); outline-offset: 1px; } .field__input:disabled { opacity: 0.5; cursor: not-allowed; } .field__textarea { height: auto; min-height: 80px; padding: var(--sp-3); resize: vertical; font-family: inherit; line-height: 1.5; } .field__help { font-size: 12px; color: var(--ink-subtle); margin: 0; line-height: 1.4; } .field--error .field__input, .field--error .field__select { border-color: var(--bad); } .field--error .field__help { color: var(--bad); } ``` ### Anti-patterns - ❌ Placeholder as label (loses on focus) - ❌ Label inside input (accessibility disaster) - ❌ No focus state (browser default is fine but design your own) - ❌ Border that disappears on focus with no replacement - ❌ Native select without styling (looks like 2005) --- ## 10. Notifications (toasts) **Used in:** Action confirmations, alerts, system messages. ### Anatomy - Fixed position (top-right or bottom-right) - Slides in from edge, fades out on auto-dismiss - Icon + title + optional description + close button - Severity variants: info, success, warning, error - Auto-dismiss after 4–6s (with progress bar) - Stack vertically if multiple ### HTML ```html

Issue created

ENG-1421 has been added to your backlog.

``` ### CSS ```css .toast { position: fixed; bottom: var(--sp-5); right: var(--sp-5); z-index: 90; display: flex; align-items: flex-start; gap: var(--sp-3); width: 360px; padding: var(--sp-4); background: var(--surface-1); border: 1px solid var(--hairline-strong); border-radius: var(--r-lg); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4); overflow: hidden; animation: toast-in 200ms ease-out; } @keyframes toast-in { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } } .toast--success { border-left: 3px solid var(--good); } .toast--warning { border-left: 3px solid var(--warn); } .toast--error { border-left: 3px solid var(--bad); } .toast--info { border-left: 3px solid var(--accent); } .toast__icon { width: 20px; height: 20px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; flex-shrink: 0; } .toast--success .toast__icon { background: var(--good); color: var(--surface); } .toast--warning .toast__icon { background: var(--warn); color: var(--surface); } .toast--error .toast__icon { background: var(--bad); color: var(--surface); } .toast--info .toast__icon { background: var(--accent); color: var(--surface); } .toast__body { flex: 1; min-width: 0; } .toast__title { font-size: var(--text-sm); font-weight: 600; color: var(--ink); margin: 0 0 2px 0; } .toast__desc { font-size: 12px; color: var(--ink-muted); margin: 0; line-height: 1.4; } .toast__close { color: var(--ink-muted); width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center; border-radius: 3px; transition: background 120ms ease, color 120ms ease; } .toast__close:hover { background: var(--surface-2); color: var(--ink); } .toast__progress { position: absolute; bottom: 0; left: 0; height: 2px; background: var(--accent); animation: toast-progress 5s linear forwards; } @keyframes toast-progress { from { width: 100%; } to { width: 0; } } ``` ### Anti-patterns - ❌ Toasts that don't auto-dismiss (stale UI) - ❌ Toasts at top-center (covers content) - ❌ Bouncy entrance animation (distracting) - ❌ No severity distinction (all toasts look the same) - ❌ No close button (user feels trapped) --- ## Component states summary Every component above must have: | State | Treatment | |---|---| | **Default** | As designed | | **Hover** | Subtle background tint or border strengthen | | **Focus-visible** | 2px accent ring, 1–2px offset | | **Active / pressed** | `transform: scale(0.98)` 80ms OR slight darken | | **Disabled** | Opacity 0.5, no hover, `cursor: not-allowed` | | **Loading** | Spinner OR skeleton, never both | | **Empty** | One sentence + one action (see §3) | | **Error** | Border + text in `--bad`, specific message | If any state is missing, the design breaks on the edges. Don't ship without all 8. --- ## Component checklist (before shipping) For every component on the page, verify: - [ ] All 8 states defined - [ ] Keyboard accessible (Tab, Enter, Escape where applicable) - [ ] Screen reader labels (`aria-label` on icon-only) - [ ] Touch targets ≥ 44×44px on mobile - [ ] No layout shift on hover/focus - [ ] Reduced motion respected - [ ] Tested in light AND dark theme --- ## What to read next - For token system → `color.md` - For typography setup → `typography.md` - For motion → `motion.md` - For anti-patterns → `anti-patterns.md` - For specific sub-styles → `minimal-ui-patterns.md`, `editorial-patterns.md`, `brutalist-patterns.md` - For final QA → `checklist.md`