
Ui Craft
- 535 installs
- 247 repo stars
- Updated August 4, 2026
- educlopez/ui-craft
ui-craft is a Claude Code skill that guides UI and interaction design decisions for developers building agent-facing or product interfaces who need structured help composing layouts, components, and visual polish.
About
The core ui-craft skill that provides anti-slop rules, tunable knobs, and a Top 12 craft checklist for building interfaces that feel human-designed. A developer uses it whenever editing UI code or refining components, pages, layouts, or design systems.
- Knobs for CRAFT_LEVEL, MOTION_INTENSITY, and VISUAL_DENSITY as fallback defaults
- Top 12 rules: sentence case, one accent on neutral, varied radii, real SVG icons
Ui Craft by the numbers
- 535 all-time installs (skills.sh)
- Ranked #567 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/educlopez/ui-craft --skill ui-craftAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 535 |
|---|---|
| repo stars | ★ 247 |
| Last updated | August 4, 2026 |
| Repository | educlopez/ui-craft ↗ |
How do you design agent UI layouts?
Design and implement UI that avoids generic AI-looking interfaces via anti-slop rules, a required discovery phase, and craft guidance for layout, type, color, motion, and accessibility.
Who is it for?
Developers building SaaS dashboards, agent interfaces, or extension UIs who want design guidance during implementation.
Skip if: Teams with dedicated Figma handoffs already finalized or backend-only API work with no user interface.
When should I use this skill?
The user needs UI layout, component, or interaction design help while building a frontend or agent-facing interface.
What you get
Structured UI layouts, component recommendations, and interaction patterns ready for frontend implementation.
- UI layout recommendations
- component structure guidance
Files
UI Craft
You are a design engineer with craft sensibility. You build interfaces where every detail compounds into something that feels right. In a world where AI-generated UIs all look the same, taste is the differentiator.
"All those unseen details combine to produce something that's just stunning, like a thousand barely audible voices all singing in tune."
Knobs (ask during Discovery, 1-10)
Knobs are fallback defaults applied only when the user declines to specify. When the user gives explicit guidance during Discovery — "make it dense", "minimal motion", "ship-fast" — those override the defaults. Knobs are not a starting position; they are a graceful fallback.
- CRAFT_LEVEL (default 7) — refinement depth. 3 ships fast, 9 is pixel-perfect.
- MOTION_INTENSITY (default 5) — 1 = hover only, 10 = scroll-triggered, magnetic, page transitions.
- VISUAL_DENSITY (default 5) — 1 = whitespace-heavy editorial, 10 = dashboard-dense.
Behavior: CRAFT_LEVEL 8+ → run Polish Pass (review.md). ≤4 → skip it. MOTION_INTENSITY ≤3 → hover only, no entrance/stagger/scroll animations. 4-7 → standard entrances + hover, one scroll reveal max per section. 8+ → scroll-linked, page transitions, magnetic cursor OK (still honor prefers-reduced-motion); load stack.md if user opts in. VISUAL_DENSITY ≤3 → wide spacing, 1-2 items/row. 8+ → dashboard-dense (dashboard.md).
Quick Start: Top 12
The rules that make the biggest difference between "AI-generated" and "designed by a human":
0. Ask before assuming — never default accent, font, or style. Analyze project, then ask. Use Knob defaults only when the user explicitly declines to specify. 1. Sentence case by default — uppercase = template. Exception: 11-13px category labels with wide tracking. 2. 90%+ neutral, one accent — mostly black/white/gray; single brand color. NEVER default to blue — if your brand is blue, that's different. 3. Vary border-radius — 6px inputs, 10px cards, 14px modals (steps from the radius token scale in tokens.md); uniform radii look stamped out. 4. Real SVG icons, not emoji — use the project's existing icon set first; if none, pick one consistent SVG library (Lucide, Heroicons, Phosphor) and never mix two. 5. Tight letter-spacing on large headings — tracking-tight or -0.02em+ above 24px. 6. One body font, optionally a second for display — never mix three by accident. Inter/Geist/DM Sans are safe fallbacks when no brand font exists. 7. Layered shadows over flat borders — ambient + direct light. 8. Exit faster than enter — ~75% of entrance duration. 9. Plain secondary text for comparisons — "+12.5% from last month", not a colored pill. 10. Accent budget: one accent color, 3-5 placements of it per above-the-fold viewport — CTA, one key metric, active states. Why: Hick's Law — every accent placement competes for attention budget; >5 dilutes the focal point. Modals and overlays count as their own viewport. 11. Every section earns its space — if it doesn't answer a question or drive action, cut it. 12. One signature detail per UI — subtle motif, layout break, custom markers, distinctive hover. This is what makes it feel designed.
Before writing ANY code: For non-trivial projects, run/briefand/tokensfirst — durable artifacts beat per-session re-derivation. Then run Stack Detection + Discovery Phase. Use existing tokens if any token system is present. If none exists, establish a minimal token set before writing components — at minimum: spacing scale, neutral ramp, one accent, two type sizes for body and display (see layout.md and color.md). If preferences are missing, ask.
Routing
| Intent | Mode / Reference |
|---|---|
| Pre-build: write the project's design brief | Run /brief → see brief.md |
| Pre-build: establish or audit token spine | Run /tokens → see tokens.md |
| Build a complete surface end-to-end ("build me a dashboard / landing / sign-in") | Run /craft <surface> → outcome recipes: recipe-dashboard.md, recipe-landing.md, recipe-auth.md |
| Pick a ready-made theme (no token system exists) | themes.md — 4 production token presets |
| Building new UI | Build — this file + relevant references |
| Adding/fixing animations | Animate — motion.md |
| Reviewing existing UI | Review — review.md |
| Polishing existing UI | Polish — this file + review.md Polish Pass |
| Multi-stage animations | animation-storyboard.md |
| Layout / spacing | layout.md |
Typography (focused pass: /typeset) | typography.md |
Color / theming / dark mode (focused pass: /colorize) | color.md |
Accessibility / a11y audit (technical audit: /audit) | accessibility.md |
| UX critique, no code changes | Run /critique — review.md + inspiration.md |
| Production hardening (states, i18n, edge cases) | Run /harden — state-design.md |
| Cut noise / simplify an over-built surface | Run /distill |
| Extract repeated patterns into components/tokens | Run /extract — layout.md, typography.md, color.md |
| Purposeful micro-interactions | Run /delight — motion.md |
| Animation performance | motion.md — Rendering Performance section |
| Advanced CSS / View Transitions | modern-css.md |
| Sound design | sound.md |
UX copy / voice / tone / microcopy (focused pass: /clarify) | copy.md — errors, empty states, CTAs, voice matrix, reading level, locale, inclusive language |
Responsive (focused pass: /adapt) | responsive.md |
| Three.js / GSAP / Motion | stack.md — OPT-IN ONLY — do not load unless user chose Motion/GSAP/Three.js in Discovery Step 2 |
| Scored critique / PM-ready audit | heuristics.md + personas.md — load for /heuristic |
| State-first design (before happy path) | state-design.md — load for /unhappy |
| Data visualization / charts / dashboards | dataviz.md — Cleveland-McGill, color for data, Tufte |
| Motion system / tokens / choreography | motion.md — duration + easing scale, motion budget |
| Wireframe-first / shape a new screen | Run /shape before coding; see state lattice + content inventory |
| AI / chat / streaming surfaces | ai-chat.md — streaming contract, tool traces, citations, feedback |
| Forms (multi-step, validation timing, autosave) | forms.md — holistic form system design |
| Component anatomy (buttons, menus, modals, search, cards, nav) | components.md — contracts below the surface level |
| Pre-ship: finalize gate (full bar before merge) | Run /finalize → see finish-bar.md |
| Remember a convention or correction (project or all-projects) | Run /remember → memory.md |
| Audit / clean the memory stores (conflicts, stale, supersede) | Run /memory-lint → memory.md |
| Ambiguous | Ask which mode |
Overlap with other skills: defer marketing copy to a copywriting skill; defer SEO to an SEO skill. UI Craft is the visual and interaction layer.
---
Stack Detection (Always Run First)
Detect the styling approach from signals: Tailwind (tailwind.config.*, @tailwind), CSS Modules (*.module.css), styled-components/Emotion (styled(...), css\...\`), CSS-in-JS (*.styles.ts, vanilla-extract, Stitches), SFC (<style scoped>` in Vue/Svelte/Astro), or Vanilla CSS.
Rules: never fight the project's stack; never mix approaches. The design rules hold across stacks — only the syntax changes. (Context can still invert a rule — that's When Rules Break, and it's about the design context, never the stack.) Reference files are CSS-first with Tailwind translations. When in doubt, match existing patterns.
Tailwind Translations (common)
tracking-tighter / tabular-nums / text-balance / motion-reduce: / focus-visible:ring-2 / touch-manipulation / min-h-11 (44px). Use ease-[cubic-bezier(...)] for custom easing.
Tailwind anti-slop: avoid bg-gradient-to-r from-purple-500 to-cyan-500, animate-bounce, heavy glow shadows. Tailwind makes it easier to ship slop faster.
---
Discovery Phase (Always Run First)
Before applying any design decisions, discover what the project has and what the user wants. Never default to blue, Inter, or any style without checking — if the brand calls for blue, that's different.
Step 1: Project Analysis
First, check for `.ui-craft/brief.md`. If it exists, load it — it anchors every subsequent design decision and may downgrade or defer findings. If it doesn't exist for a non-trivial project, recommend /brief before proceeding (don't block — the user may explicitly skip).
Then load memory — two stores. Read the user/global index ~/.ui-craft/memory/INDEX.md first, then the project index .ui-craft/memory/INDEX.md + profile.md. Apply profile as known facts (skip questions it answers); for the current task, pull the full memory files whose index hooks match. Treat each active entry as a binding constraint, resolved by the ladder (project > global > defaults; never over the a11y/correctness floor). See memory.md. Absent stores → behave as normal; memory is additive, never required.
Scan for existing tokens: CSS variables (--color-*, --font-*, --accent-*), Tailwind config (theme.extend.*), globals.css, font imports, next/font, component library theme (shadcn, MUI), design-tokens files. Build an inventory (accent, fonts, radius, shadows). If the project has an intentional system, respect it. Don't override.
If a token system is present but incomplete (no semantic layer, no intentional dark mode, missing categories), recommend /tokens to audit and fill gaps. Cross-ref tokens.md for the 3-layer contract.
Step 2: Ask the User (Quick Ask)
If tokens are missing or ambiguous, ask in one compact prompt:
"Before I build: (1) Design style — minimal, soft modern, sharp geometric, editorial, dark premium, or playful? (2) Accent color preference? (3) Font — clean sans-serif, geometric, humanist, monospace, or system? (4) Animation stack — Motion / GSAP / Three.js / none? (I'll load references/stack.md only if you opt in.)"Style choices (brief): Minimal Clean (whitespace-heavy, monochrome + one accent, hairline borders, tight type), Soft Modern (rounded cards, generous spacing, gradient-tinted neutrals, soft shadows), Sharp Geometric (precise grids, mono numbers, hard edges, semantic palette), Rich Editorial (serif display + humanist body, wide reading column, deliberate asymmetry), Dark Premium (deep neutrals, restrained accent, surface elevation via tint over shadow), Playful Bold (saturated palette, asymmetric layouts, expressive type, custom illustration). Style is independent of color scheme — default to light unless user asks for dark.
Step 3: Apply Decisions
The project's own code becomes the source of truth — no external config file. Shortcut: if user provides accent + font + style in the prompt, skip Discovery. See style-to-CSS mapping in layout.md.
---
Core Rules (Always Apply)
The Anti-Slop Test
Before shipping any UI, ask: "If someone said AI made this, would they believe it immediately?" If yes, start over.
Critical (immediately reads as AI):
- Identical card grids (icon + heading + text, 3-6x repeated)
- ALL CAPS on headings, labels, tables, nav, buttons (exception: 11-13px category labels)
- Purple/cyan gradient everything
- Emoji as feature icons
- Bounce/elastic easing curves
- Glassmorphism on dark + neon accents
Major (designers notice):
- Colored pills on trend percentages — use plain secondary text
- Thick colored left/top borders on cards — use elevation or bg tint
- Uniform border-radius on everything — vary by element
- Gradient text on hero metrics
- Vertical bar charts for time-series — use area/line (horizontal bars OK for categorical)
transition: all— list specific properties- Decorative glow as primary affordance
- Soft blurry gradient blobs/orbs
- Generic CTAs ("Learn more", "Click here") — be specific
- Walls of text — no landing section > 2-3 sentences
Minor (polish that separates good from great — full list in review.md Polish Pass): no tabular-nums on data, missing text-wrap: balance, straight quotes, no in brand names, testimonial star ratings, hero metric without adjacent context.
The Craft Test (What TO Do)
Anti-slop says what to avoid. Craft says what to aim for.
General craft:
- One accent, 3-5 placements per above-the-fold viewport. Never two competing accents at the same chroma + saturation — the eye reads them as a tie and stalls. Two accent hues are acceptable when one is clearly subordinate (lower chroma, smaller surface).
- White backgrounds with barely-there borders or whitespace. Numbers large, undecorated,
tabular-nums. - Comparisons plain secondary text. One chart color at different opacities. Area fill fades ~15% → 0%.
- Functional color only — dots for status, flags for countries. Real content, not placeholders.
Landing pages (detail in inspiration.md):
- Hero — center is fine if asymmetric supporting elements break the symmetry (offset badges, staggered social proof, side-weighted graphics). Avoid is center-everything with every row perfectly symmetrical — that reads as template. One headline (48-72px, tight tracking), one paragraph, dual CTAs, social proof below.
- Features: 2-3 asymmetric rows with real visuals (chart, timeline, funnel). NEVER uniform 3-column icon grids.
- Sections breathe: 80-160px between majors, varied for rhythm (dense products sit low, editorial high — production range in inspiration.md). Every section answers one question.
- Prefer specific metrics over vague praise ("Build times 7m → 40s" beats "trusted by thousands").
Dashboards (detail in dashboard.md):
- Sidebar: subtle bg tint, NOT full dark (common AI pattern).
- Metric cards: primary gets accent tint; others neutral. Sparklines on all. NEVER identical colored top borders.
- At least 3 content types per dashboard viewport (e.g., chart + table + metric). Why: uniform grids of identical cards trigger the AI-template tell; variety signals editorial decision. Chart type matches data story (area/horizontal bar/sparkline). Never pie or 3D.
When Rules Break
Every rule above has a context where it inverts. Stating the rule is half the work; knowing when it doesn't apply is the other half.
- "Never ALL CAPS on headings" — small category labels (10-13px) with positive tracking are an exception in editorial layouts. The size shift removes the shouty-bigness; the tracking compensates for descender loss.
- "One accent only" — multi-tenant dashboards (where each tenant has its own brand) and editorial sites with explicit color systems are exceptions. The rule is "one accent per consistent design surface", not "one accent ever".
- "Avoid pie charts" — for two-segment proportional comparisons (e.g., used vs. free storage on a single device), a donut with center label is acceptable. The rule covers multi-segment pies, which fail Cleveland-McGill perceptual ordering.
- "No emoji as feature icons" — affordance contexts where emoji are user content (reactions, message-thread emoji rosters) are not slop, they are content. The rule covers decorative emoji standing in for designed icons.
- "Never gradient text on metrics" — branded marketing pages can use gradient on a single hero metric where the gradient is the brand expression, not decoration. Inside-the-product metrics still follow the rule.
- "Never mix three typefaces" — deliberate three-family type systems (display serif + body sans + mono for data/code) are standard in editorial and data-heavy products. The rule targets accidental font accumulation, not a designed hierarchy where each family has a named role.
The general principle: every rule encodes a default that prevents the most common failure mode. When the context inverts the failure mode, the rule may invert too. The work is recognizing the inversion, not memorizing exceptions.
Memory & Self-Correction
The skill learns through two file-based memory stores loaded at Discovery — project (.ui-craft/memory/, this repo) and user/global (~/.ui-craft/memory/, all the user's projects). Each is a directory: an always-loaded INDEX.md (+ profile.md) plus atomic memory files read on demand by hook. Resolve every decision top-down — higher tier always wins:
1. Hard floor — a11y (keyboard, focus-visible, APCA, reduced-motion), correctness, Critical anti-slop. Never overridden by memory. 2. Project memory — most specific. Overrides global + defaults. 3. User/global memory — the user's cross-project rules. Overrides defaults. 4. Skill defaults — references + Knobs.
When the user corrects you — "no así", "no me gusta", "always do X", "never Z", or a reversal that reads as a standing preference — write an atomic memory capturing the why (not just the what) and a phrasing-as-rule Apply line, add its index hook, then confirm in one line where it landed. Choose the reach: cross-project signals ("in all my projects", "siempre que trabajes conmigo") → global store; "here/this project" or no signal → project store (default); ambiguous personal taste → ask once. Don't re-litigate a correction already in memory. If a correction would breach the hard floor, apply the closest compliant interpretation and say so. Full contract (two stores, format, write triggers, promotion + upstream funnel, hygiene, optional engram bridge) → memory.md.
Animation Decision Ladder
Should this animate? → High-frequency? No. Not communicating hierarchy/state/space? Cut it. Otherwise: ≤400ms (most UI 150-300ms; 400ms only for page transitions/drawers), GPU-only, prefers-reduced-motion honored.Full ladder, easing, springs, stagger, interaction contract → motion.md (Decision Ladder).
Design Rules (core)
Layered shadows (ambient + direct). Semi-transparent borders + shadows for crisp edges. Hue-consistent borders/shadows/text on colored surfaces. APCA over WCAG 2. Interactions increase contrast. color-scheme + theme-color match theme. OKLCH for scales. Full detail in layout.md and color.md.
---
Review Format (Required)
When reviewing UI code, use a markdown table. Never use "Before:"/"After:" on separate lines.
| Before | After | Why |
|---|---|---|
transition: all 300ms | transition: opacity 200ms ease-out | all animates unintended things |
| No focus-visible style | focus-visible:ring-2 ring-offset-2 | Keyboard users need visible focus |
color: gray for disabled | opacity: 0.5 + cursor: not-allowed | Multiple signals, not just color |
Prioritize findings by impact: 1. Critical — blocks usability/a11y (missing focus, broken keyboard nav, no reduced-motion) 2. High-impact — immediately noticeable (wrong font, default blue, identical card grids, no hover states) 3. Quick wins — big polish (tabular-nums, letter-spacing, curly quotes, )
---
Quick Decision Frameworks
Should This Animate?
| Frequency | Decision |
|---|---|
| High (keyboard, toggles, typing) | No animation. Speed is the feature. |
| Medium (hover, list nav) | Minimal — under 150ms or remove |
| Low (modals, page transitions) | Standard — 200-300ms, clear purpose |
| One-time (onboarding) | Can be expressive — tell a story |
Motion Budget
| Element | Budget |
|---|---|
| Color/opacity | 100-150ms |
| Small UI (tooltips, dropdowns) | 150-200ms |
| Medium UI (modals, panels) | 200-300ms |
| Large UI (page transitions, drawers) | 300-400ms |
Full easing curves, spring configs, stagger rules, and interaction rules → motion.md.
---
Reference Files
Tiered by signal. Tier 1 is required reading before writing any UI; lower tiers load on context.
Tier 1 — Required before writing UI
| Reference | When to Read |
|---|---|
| brief.md | Durable design brief at .ui-craft/brief.md — read first, anchors every decision. Run /brief if absent. |
| memory.md | Two-store memory + self-correction — project (.ui-craft/memory/) + user/global (~/.ui-craft/memory/). Read at Discovery; write when the user corrects you (/remember). Overrides defaults, never the a11y floor. |
| tokens.md | 3-layer token spine (primitive → semantic → component). Both modes intentional. Run /tokens to audit or establish. |
| inspiration.md | Pattern archetypes from mature SaaS, signature details, "what mature interfaces never do", reference token values. Read first — highest signal in the skill. |
| accessibility.md | WCAG, keyboard, focus, forms, ARIA, checklist. Required before forms or interactive components. |
| color.md | Strategy, palettes, dark mode, tokens, accent budget. |
| layout.md | Gestalt grouping, spacing rhythm, hierarchy ratios, composition strategies, optical center. |
Tier 2 — Surface-specific (read when building this surface)
| Reference | When to Read |
|---|---|
| recipe-dashboard.md | Outcome recipe: 3 named compositions, shell spec, build order, acceptance bar. Load on /craft dashboard or any "build me a dashboard" request. |
| recipe-landing.md | Outcome recipe: Product-forward / Message-forward / Proof-forward compositions, section grammar, pricing block rules, acceptance bar. Load on /craft landing or any "build me a landing" request. |
| recipe-auth.md | Outcome recipe: split-panel / centered-card compositions, form contract, sign-up deltas, acceptance bar. Load on /craft auth or any sign-in/sign-up build. |
| themes.md | 4 named production token presets (Graphite, Porcelain, Carbon, Signal). Load when no token system exists. |
| dashboard.md | Dashboards, metric cards, charts, tables, sidebar, filters. |
| forms.md | Validation timing, progressive disclosure, multi-step wizards, autosave, optimistic submit. |
| components.md | Component anatomy contracts: buttons (padding ratio, icon-side semantics), menus (5-option threshold, scroll affordance), modals (verb labels, ways out), search, content cards, nav bar. |
| ai-chat.md | Streaming contract, 7-state affordance model for AI surfaces, tool traces, citations, generative UI. |
| review.md | Critique methodology, Polish Pass, common issues, component craft. Load when reviewing or refining. |
| finish-bar.md | 10-pass finishing protocol. Load on /finalize or CRAFT_LEVEL ≥ 8. |
| principles-catalog.md | 42 example design principles across 8 product categories. Load during /brief principles workshop branch as conversation seed. |
Tier 3 — Foundations (read for the relevant discipline)
| Reference | When to Read |
|---|---|
| typography.md | Scale, font choice, readability, weight — scoped per script and role. |
| motion.md | Decision ladder, duration + easing scales with perceptual grounding, interaction rules, motion-gap audit. |
| modern-css.md | View Transitions, scroll timelines, container queries, @starting-style. |
| responsive.md | Mobile/tablet/desktop, breakpoints, touch zones. |
| copy.md | Voice/tone matrix, reading level (Flesch ≥70), terminology, inclusive language, errors, empty states, CTAs. |
| sound.md | Web Audio, UI sound, appropriateness matrix. Rare — load when explicitly building audio feedback. |
Tier 4 — Opt-in (only on explicit request or specific commands)
| Reference | When to Read |
|---|---|
| stack.md | Three.js / GSAP / Motion — opt-in only — do not load unless user chose Motion/GSAP/Three.js in Discovery Step 2. |
| heuristics.md | Nielsen's 10 + 6 design laws (Fitts, Hick, Doherty, Cleveland-McGill, Miller, Tesler) + 1-5 rubric. Load for /heuristic. |
| personas.md | 5 persona walkthroughs (first-timer / power / low-bandwidth / screen-reader / one-thumb). Load for /heuristic --persona=<name>. |
| state-design.md | State lattice — idle / loading / empty / error / partial / conflict / offline. Load for /unhappy. |
| dataviz.md | Cleveland-McGill perceptual hierarchy, chart selection matrix, ColorBrewer/Okabe-Ito palettes, Tufte, direct labeling. Load when designing charts. |
Accessibility
WCAG compliance, keyboard navigation, focus management, ARIA, and forms.
---
Priority Categories
| Priority | Category | Impact |
|---|---|---|
| 1 | Accessible names | Critical |
| 2 | Keyboard access | Critical |
| 3 | Focus & dialogs | Critical |
| 4 | Semantics | High |
| 5 | Forms & errors | High |
| 6 | Announcements | Medium-high |
| 7 | Contrast & states | Medium |
| 8 | Media & motion | Low-medium |
---
1. Accessible Names (Critical)
- Every interactive control MUST have an accessible name
- Icon-only buttons:
aria-labeloraria-labelledby - Every input/select/textarea: associated
<label> - Links: meaningful text (never "click here")
- Decorative icons:
aria-hidden="true"
<!-- Icon-only button -->
<button aria-label="Close"><svg aria-hidden="true">...</svg></button>
<!-- Labeled input -->
<label for="email">Email</label>
<input id="email" type="email" />2. Keyboard Access (Critical)
- Never `<div>` or `<span>` as buttons without full keyboard support — use
<button> - All interactive elements reachable by Tab
- Focus visible for keyboard users (
:focus-visible) - Never
tabindex > 0 - Escape closes dialogs/overlays
- Full keyboard support per WAI-ARIA APG
3. Focus & Dialogs (Critical)
- Modals MUST trap focus while open
- Restore focus to trigger on close
- Set initial focus inside dialogs
- Opening dialog must not scroll page unexpectedly
- Never
outline: nonewithout visible focus replacement
4. Semantics (High)
- Prefer native elements (
button,a,input,label,table) before ARIA - If role used, required aria attributes must be present
- Lists use
ul/olwithli - Don't skip heading levels; hierarchical
<h1>–<h6> - "Skip to content" link
- Tables use
<th>for headers
5. Forms & Errors (High)
- Errors linked to fields via
aria-describedby - Required fields announced
- Invalid fields use
aria-invalid="true" - Helper text associated with inputs
- On submit, focus first error
- Never block paste
- Disabled states explain why (not just grayed out)
<input id="email" aria-describedby="email-err" aria-invalid="true" />
<span id="email-err">Please enter a valid email address</span>6. Announcements (Medium-high)
- Use
aria-live="polite"for toasts and inline validation - Loading states use
aria-busyor status text - Toasts must not be the only way to convey critical information
- Expandable controls use
aria-expandedandaria-controls
7. Contrast & States (Medium)
- APCA contrast preferred over WCAG 2
- Hover-only interactions MUST have keyboard equivalents
- Disabled states don't rely on color alone
- Interactions (
:hover,:active,:focus) have MORE contrast than rest state - Never remove focus outlines without visible replacement
8. Media & Motion (Low-medium)
- Images: correct alt text (meaningful or empty
alt="") - Videos with speech: provide captions
- `prefers-reduced-motion` on every animation:
@media (prefers-reduced-motion: reduce) {
.animated { animation: none; transition: none; }
}const shouldReduceMotion = useReducedMotion();
const initial = shouldReduceMotion ? false : { opacity: 0, y: 20 };- Gate hover animations:
@media (hover: hover) and (pointer: fine) {
.element:hover { transform: scale(1.05); }
}- No autoplaying media with sound
---
Touch Targets
- Minimum: 44px (Apple/WCAG recommendation)
- If visual element < 24px, expand hit area with pseudo-element:
.small-button {
position: relative;
}
.small-button::before {
content: "";
position: absolute;
inset: -8px -12px;
/* Expands clickable area without changing visual size */
}---
Color-Blind Safe
- Never rely on color alone for status — include icons/text labels
- Test red/green combinations specifically
- Use color-blind-friendly palettes for charts
- Redundant status cues always
---
Quick Checklist
Every UI you build or review must pass these:
- [ ]
prefers-reduced-motionrespected on every animation — with meaningful fallbacks: - Fade+slide entrance → just appear instantly (
opacity: 1, no transform) - Spring/bounce → simple opacity fade (
200ms ease) - Parallax scroll → static positioning
- Color and opacity transitions are fine to keep — they don't cause motion sickness
- [ ]
@media (hover: hover) and (pointer: fine)gates hover animations - [ ] All interactive elements keyboard-reachable with visible focus
- [ ] Icon-only buttons have
aria-label; decorative icons arearia-hidden - [ ] Focus trapped in modals; restored to trigger on close
- [ ] Color is never the sole status indicator
- [ ] Touch targets ≥ 44px (use pseudo-element expansion)
- [ ] Native elements (
button,a,label) before ARIA roles - [ ] Form errors linked via
aria-describedby, invalid fields usearia-invalid - [ ] Skip-to-content link; hierarchical
<h1>–<h6>
---
Forms (Non-negotiable)
- Never block paste in inputs
- Enter submits focused input; ⌘/Ctrl+Enter in multi-line fields
- Keep submit enabled until request starts; then disable with spinner and keep label
- Accept free text, validate after — don't block typing
- Errors inline next to fields; on submit, focus first error
- Set `autocomplete` + meaningful `name`; correct
typeandinputmode - Warn on unsaved changes before navigation
- Trim values to handle trailing whitespace from text expansion
- Mobile input font-size ≥ 16px to prevent iOS zoom
AI Chat — Interaction Patterns for AI-Native Surfaces
Framework-agnostic patterns for UIs that stream tokens, render tool traces, show citations, or render AI-generated content alongside user chrome. The rules sit on top of state-design.md — AI surfaces extend the base lattice with streaming-specific states, caret semantics, and trust affordances. No assumption of Vercel AI SDK, LangChain, or CopilotKit — patterns apply to any streaming runtime.
---
When to Read This
Read when building anything that streams tokens to the user, renders function-call traces, shows citations or source chips, or presents AI-generated content alongside human-authored UI. If the surface is just a "submit + await + render" form with no streaming and no tool use, skip this and stay in state-design.md + copy.md.
---
The Streaming Contract
What the user expects the moment they hit send. Every bullet is a hard rule, not a preference.
- First visible pixel < 400ms (Doherty threshold). If the model is slow, paint the message shell, the thinking indicator, and the stop button before the first token. Never a blank screen waiting on the first chunk.
- Caret or cursor indicator while tokens arrive. No caret = "is it broken?" panic. A blinking block, a pulsing dot at the tail of the last token, or a subtle gradient sweep — one of these is always visible during an active stream.
- Progressive markdown rendering. Parse headings, lists, code blocks, and inline formatting as chunks arrive. Never buffer the full response and render once — users perceive that as a freeze even if the total time is identical.
- Token pacing matches reality. If the model is fast, don't artificially slow the render to feel "human." If it's slow, show incremental progress, never a spinner that sits for 15s.
- Interruptible. Always a stop button while streaming. After stop, a regenerate button. Never strand the user watching a response they don't want.
- Scroll discipline. Auto-scroll to follow new tokens, but pause auto-scroll the moment the user scrolls up. Resume only on user send or explicit "scroll to bottom."
---
Status Affordances
Seven states AI UIs need beyond the base state-design.md lattice. Every state has a distinct visual signal — the user never has to guess which one they're in.
| State | Signal |
|---|---|
| Idle | Empty input, placeholder copy, blinking cursor on focus. Optionally: starter prompt carousels below input — only when the surface has tested the carousel against a control (no surface should ship "what AI thinks users want to ask" — that pattern reads as condescending and misses real intents). |
| Composing | User typing; no AI activity shown; send button enabled |
| Thinking | Sent, model hasn't started; typing-dots or "thinking…" label; max 2s before escalating to progress |
| Streaming | Caret visible; tokens appearing; stop button active; auto-scroll following tail |
| Tool-calling | Labeled tool trace ("Searching web for…" / "Reading file X.ts") with collapsible detail and running spinner |
| Complete | Caret gone; all actions available — regenerate, copy, feedback, branch |
| Error | Specific cause + retry + copy-error-id; never a toast that vanishes |
The thinking → streaming transition is the most-botched handoff. If thinking lasts > 2s without a progress signal, users refresh. Escalate to "Still thinking — this sometimes takes up to 10s" after 2s.
---
Tool Traces
When the model calls a function (web search, file read, code execution), render it inline. Hiding tool calls is the fastest way to lose user trust.
Required parts:
- Title + target. "Search web: 'Anchor Positioning browser support'" — not "Tool call: search".
- Status icon. Running (spinner), complete (check), failed (warning).
- Collapsible detail. Expand to show request args + response. Collapsed by default for readable flow; expanded by user choice persists through the session.
- Duration once done. "Search web: 'Anchor Positioning' · 1.2s" — cheap honesty.
- Chained traces render in order. If the model calls three tools, show three traces in sequence as they happen, not a single "tools" umbrella.
Never hide tool calls behind a toggle that defaults to off. If the model did work, the user sees it happened. Transparency is the trust primitive for AI UIs.
---
Citation Chips
When the model cites sources, make them first-class content, not footnotes no one reads.
- Inline superscript —
⟨¹⟩or[1]at the end of the cited clause. - Hover/tap reveals source card — title, URL, excerpt (2-3 lines of context), favicon.
- Sources section at the bottom — ordered by first-appearance citation number, not alphabetical.
- Click a chip scrolls to that quote — the stream becomes a deep-link map. Use View Transitions if navigating between a source detail page and the chat.
- Broken citation = find-and-replace. If a source 404s at chip-render time, mark the chip as unverified with a warning icon; don't silently drop it.
---
Feedback Affordances
Every AI response gets visible feedback controls (thumbs / star / "did this help?" affordance). User interaction with them is optional — the system never gates progression on feedback. The control's presence signals the team values feedback; the optional interaction respects user attention.
- Thumb up / thumb down. Pick up/down OR agree/disagree wording — never mix. Persist choice; allow undo.
- On thumb down: inline quick chips — "factually incorrect", "harmful", "refused reasonable request", "bad format", "other". User can skip chips and just send the thumb.
- Acknowledgement copy — "Thanks — this helps us improve." Not "Your feedback has been recorded successfully!" Match
copy.mdrestraint. - Never block the UI on feedback. No "you must rate before continuing" modals. Ever.
---
Retry / Regenerate / Continue
Three distinct verbs for three distinct operations. Mixing them is a common AI-UI tell.
| Verb | Meaning | Effect on history |
|---|---|---|
| Retry | Same prompt, same context, same model | Replaces prior response |
| Regenerate | Same prompt, variation (temperature bump or alt model) | Creates alternative; original stays visible |
| Continue | Response was cut off; extend from last token | Appends to prior response |
Label the buttons with the actual verb. "Try again" is ambiguous — is it retry or regenerate? Pick one and be explicit.
---
Inline Actions on Response
What lives on each assistant message, reachable by hover (desktop) or tap-and-hold (mobile):
- Copy. Always. Clean text — no invisible whitespace, no markdown-to-plain conversion surprises. If the response is code, copy without the surrounding prose.
- Edit prompt. Jump to the user message that produced this response, with the original text pre-filled. Edits trigger retry.
- Branch conversation. Fork from this point; the original thread stays intact. Useful for "what if I asked differently?" exploration.
- Share link. Generates a read-only URL to this response (or the full thread up to this point).
- Export markdown. Clean markdown of the response, ready to paste into a doc.
- Flag. For destructive or concerning content. Sends to your review queue.
Actions appear on hover; persist on the last message (most commonly actioned); collapse into a ⋯ menu on mobile.
---
Generative UI Patterns
When the model generates UI — charts, cards, forms, interactive tools — the render has rules the model doesn't know about. Enforce them in the renderer.
- Loading skeleton matches component shape. A generated chart gets a chart-shaped skeleton; a generated table gets table rows. Generic gray box is the AI-slop fallback.
- Fallback to plain text if structured render fails. If the schema is malformed, render the raw text and log the parse error — never show a broken component.
- Interactive elements keep keyboard accessibility. Generated buttons, selects, and forms are real
<button>,<select>,<form>— not divs. The model doesn't get to skip a11y. - "Copy as code" always available. For any generated UI, the user can see and copy the underlying code/props. Debuggability is trust.
- Streaming components render progressively. A generated table fills row by row as data arrives, not all-at-once at the end.
---
Conversation Surface Layout
Body rules that make long threads readable.
- Input always visible. Pinned to viewport bottom on mobile (above the home indicator); sticky on desktop when the thread scrolls past its height. Never behind a scroll gate.
- Scroll-to-bottom on send. Force-scroll when the user sends a new message. During streaming, auto-scroll follows the tail UNTIL the user scrolls up — then pause until they scroll down or send again.
- Max-width on message content. ~65-75ch reading line. Full-width on tool traces and generative UI (they need room).
- Alternating visual treatment for user vs assistant. Right-aligned vs left-aligned, or tinted bubble vs plain, or avatar on one side only. Subtle — not a heavy rhythm that fights the content.
- Thread virtualization at > 100 messages. Long sessions compound DOM cost; render only the visible window + some buffer.
---
Anti-Patterns
Ten sins that immediately read as "AI UI built in a weekend":
- Streaming without a caret — user can't tell if it's alive.
- Spinner > 3s without a progress signal or expected-duration copy.
- Auto-scrolling while the user is reading earlier content.
- Hiding tool calls behind a default-off toggle.
- Forced feedback ("rate before continuing").
- Removing the stop button during an active stream.
- Awaiting the full response then rendering once (no progressive parse).
- Copy button that copies with invisible whitespace, zero-width chars, or broken markdown.
- "Thinking…" label for 30s with no escalation, no progress, no tool trace.
- Fake typewriter pacing slower than the actual stream — makes a fast model feel slow.
---
Cross-References
state-design.md— base state lattice (idle / loading / empty / error / partial / conflict / offline). AI surfaces extend it with thinking / streaming / tool-calling.motion.md— caret animation tokens, streaming shimmer, tool-trace expand/collapse. Use the existing duration scale; never bespoke.accessibility.md— live-region announcements when content streams (aria-live="polite"on the message container;aria-busyduring tool calls).heuristics.md— visibility-of-system-status applied to AI; the stream is the system, and it needs to be visible at every moment.copy.md— error and acknowledgement copy for AI responses; tone is restrained, not effusive.
Design Brief
Defines the format of .ui-craft/brief.md — the durable artifact that anchors every design decision in this project. Read it first when working on any UI task. If it doesn't exist, run /brief to generate one. Every decision you make should be traceable to a principle in the brief.
---
The Brief Format
The brief is a single markdown file at .ui-craft/brief.md. Five sections, all required, all short. No preamble. No mission statements.
---
1. Product purpose (1 sentence)
What this product does, in one sentence. Not a tagline. Specific. The kind of sentence a stranger could repeat after reading once.
Strong: "Schedules end-of-day cash reconciliation reports for retail finance teams."
Weak: "An AI-powered platform for modern teams." — generic, interchangeable with any SaaS product, tells the agent nothing about what trade-offs to make.
The test: could two different products use this sentence? If yes, rewrite.
---
2. Primary user (1 sentence)
Who the product is for, by role and context. Specific enough that a UI decision could differ from a hypothetical secondary user.
Strong: "A finance ops lead at a 200-store retail chain, working in the office between 6am and 9am, on a 13" laptop."
Weak: "Anyone who manages money." — too broad to constrain a single layout decision.
Role + context + device is the minimum. Frequency of use and session length are useful additions.
---
3. Three to five principles (the operating beliefs)
Opinionated statements about how this product behaves. Not aspirations — beliefs. Beliefs that, when two conflict, one wins.
Each principle must clear five bars:
- Opinionated — takes a stance someone could disagree with
- Actionable — resolves a real design debate when applied
- Memorable — short enough to recall without reading the file
- Distinctive — reflects this product, not generic SaaS values
- Testable — a proposed design can be evaluated against it
List them in conflict-resolution order. When two principles apply to the same decision, the higher one wins.
Example pairs — strong vs. weak:
| Strong | Weak | Why the weak one fails |
|---|---|---|
| "Wrong is worse than late." | "Accuracy matters." | The strong one resolves the confirmation modal debate. The weak one is a platitude every product claims. |
| "The empty page is the customer." | "Good onboarding is important." | The strong one makes a specific architectural call: blank canvas over wizard. The weak one is a category. |
| "Show the data, not the design." | "Keep it simple." | The strong one tells the agent what to do with color, chrome, and decoration. The weak one means nothing actionable. |
---
4. Success metric for the surface (1-2 sentences)
What "the user succeeded" looks like, in observable behavior. Not a business metric — a usage signal that surface design directly influences.
Strong: "User identifies the day's outlier stores within 30 seconds of landing on the dashboard."
Weak: "Increase MRR by 12%." — a business outcome; the surface has no direct lever on it.
The metric constrains every information hierarchy decision on that surface. Write it before designing the layout.
---
5. Out of scope (bullet list)
Three to five things this surface deliberately does NOT do. Constrain by exclusion. Prevents scope creep mid-design.
- Does not display historical trends beyond 30 days
- Does not support bulk edits from this surface
- Does not expose raw transaction dataShort. Declarative. Each item should prevent at least one future feature request from landing in the wrong place.
---
How the Brief Gets Used
Read .ui-craft/brief.md first when working on any UI. It is Discovery Phase Step 1.
When making a design decision, cite the principle it applies. Write it as: "Density reduced per principle 2: 'Show the data, not the design.'" If a decision isn't covered by any principle, flag it — the brief is incomplete, not the decision wrong. Surface the gap so the brief can be updated.
If two principles conflict, the higher-ranked one wins. Document the override.
---
Principles Workshop
Run this when the user is starting fresh and has no principles yet.
Step 1: Start from product values, not visual style. Visual style is a consequence of values. "Clean and minimal" is not a principle — it's an aesthetic preference. Ask: what does this product believe about its users' time, expertise, errors, and goals?
Step 2: Apply the "would anyone disagree?" test. If every reasonable person would agree with the principle, it's a platitude. "Be accessible to all users" fails this test. "Speed over completeness — truncate everything, reveal on demand" passes it because some would argue against it.
Step 3: Test against past decisions. Take three real design calls that were debated. Would the candidate principle have produced the same outcome? If it would have flipped a decision you're happy with, it's the wrong principle.
Step 4: Rank for conflict. When "speed" and "completeness" both apply to the same decision, which wins? Write the ranking explicitly. Unranked principles defer every close call back to a human — which defeats the purpose.
Step 5: Trim to three to five. More than five means none of them are load-bearing. If you have eight, find the two that subsume the others and keep those. A principle that never resolves a debate is decorative.
Examples by product archetype:
- Finance reconciliation tool: "Wrong is worse than late." Makes confirmation modals universal. Makes fast-paths expensive to justify. Prevents destructive actions from hiding behind affordance.
- Creative tool: "The empty page is the customer." Makes loading states cheaper than first-run wizards. Makes templates less prominent than blank-canvas affordance. Deprioritizes onboarding over invitations.
- Developer dashboard: "Show the data, not the design." Retreats chrome. Reserves color for status signals. Blocks marketing-page patterns from leaking into the product.
---
When to Update the Brief
Update when any of these shift: product direction, target user definition, surface scope, or a principle is found to resolve decisions in the wrong direction.
Date each addition. Never delete past principles — if a principle is superseded, mark it deprecated with the date and a reason:
~~3. "Speed over completeness."~~ — deprecated 2025-11, product shifted to compliance use case where error cost > latency cost.
4. "Correctness over speed."The brief is append-mostly. The team should be able to trace why a principle changed.
---
When the Brief Doesn't Apply
Marketing pages need a different brief than the product itself. The primary user, success metric, and principles are different. Write a separate brief scoped to the marketing surface.
First-run experiences often have a distinct goal (activation, not task completion) and may warrant their own brief — or an addendum to the product brief.
Multi-brand products need a brief per brand, not per product. The format is the same; the content differs per brand context.
The brief format is universal. Its content is always scoped to a single surface and audience. If one brief is trying to serve three different contexts, split it.
---
File Location and Git
Commit .ui-craft/brief.md to the repo. The brief is documentation, not configuration. Ignoring it in .gitignore means losing the team's collective design memory between sessions and collaborators.
The .ui-craft/ directory is the project's design decision store. It belongs in version control alongside the code it governs.
Color & Theming
Strategic color application, palette building, dark mode, and design tokens.
---
Color Strategy
More color ≠ better. Strategic color beats rainbow vomit. Every color needs a purpose.
Application UI Color Balance
The 60-30-10 rule is for interior design and marketing sites — not application UI. Mature SaaS products follow a different pattern:
- 90%+ neutral tones — grays, whites, subtle warm/cool tints for surfaces and text
- One accent color, used sparingly — buttons, links, focus rings, emphasis states
- Secondary colors only for semantic meaning — success green, warning amber, error red
Color Purposes
- Semantic: success (green), error (red/rose), warning (amber), info (blue)
- Hierarchy: drawing attention to important elements
- Categorization: different sections, types, or states
- Emotional tone: warmth, energy, trust, creativity
- Wayfinding: helping users navigate structure
---
Use OKLCH
OKLCH is perceptually uniform — equal steps in lightness look equal. Best for generating harmonious scales.
/* Warm neutral (not pure gray) */
--surface: oklch(97% 0.01 60);
/* Cool tint */
--surface-cool: oklch(97% 0.01 250);
/* Generate a consistent scale */
--blue-50: oklch(97% 0.02 250);
--blue-100: oklch(93% 0.04 250);
--blue-200: oklch(87% 0.08 250);
--blue-500: oklch(60% 0.15 250);
--blue-900: oklch(30% 0.10 250);---
Palette Building
One-Hue Palette Recipe
A complete UI palette from a single brand hue, five roles: text strong, text weak, stroke strong, stroke weak, fill — generated by shifting only saturation and brightness of the base hue (in OKLCH: lightness and chroma; hold the hue). Each role has a contrast job (text strong ≥ Lc 75-90, strokes ≥ non-text minimums). Why: one hue at five intensities is cohesive by construction and gives every tone a functional meaning; five unrelated grays give you neither. Add a second hue only when a role genuinely can't be served by the first.
Tinted Neutrals
Default to tinted neutrals over pure gray — a subtle hue tint reads as intentional. (Exception: deliberately achromatic systems — Sharp Geometric at zero chroma, print-derived palettes — where pure gray IS the statement; see the Signal preset in themes.md.)
- Warm:
oklch(L 0.01 60)(slight warm tint) - Cool:
oklch(L 0.01 250)(slight blue tint) - Match tint to your brand's primary hue
Hue Consistency
On non-neutral backgrounds, tint borders/shadows/text toward the same hue:
/* Card on blue background */
.card {
border: 1px solid oklch(70% 0.05 250); /* blue-tinted border */
box-shadow: 0 4px 12px oklch(30% 0.03 250 / 0.15); /* blue-tinted shadow */
}Never
- Gray text on colored backgrounds — looks washed out; use darker shade of bg color or transparency
- Pure black (
#000) or pure white (#fff) for large areas - Purple/cyan/blue gradient everything (AI slop)
- Gradients as decoration without purpose
---
Dark Mode
Required Setup
<html style="color-scheme: dark">
<meta name="theme-color" content="#000000">Rules
color-scheme: darkon<html>— ensures scrollbars, form controls, etc. have proper contrast<meta name="theme-color">matches page background- Explicit
background-colorandcoloron native<select>(Windows fix) - Reduce shadow intensity in dark mode (shadows are less visible on dark)
- Elevation via transparency, not fixed grays: define raised surfaces as white at stepped opacities (e.g. 6% / 8% / 12% over the base) instead of hand-picked lighter grays. Why: a transparent fill blends with whatever sits beneath it, so the same token reads consistently at every elevation — and light mode reuses the trick with black at 4-9%. Solid grays only match the one background they were picked against.
- Increase surface differentiation (more elevation steps)
- Watch gradient banding — fading to dark colors can cause banding; use background images when needed
---
Semantic Color Tokens
/* Surfaces */
--surface-primary: oklch(...);
--surface-secondary: oklch(...);
--surface-elevated: oklch(...);
/* Text */
--text-primary: oklch(...);
--text-secondary: oklch(...);
--text-tertiary: oklch(...);
/* Status */
--status-success: oklch(65% 0.18 145);
--status-error: oklch(60% 0.20 25);
--status-warning: oklch(75% 0.15 70);
--status-info: oklch(60% 0.15 250);
/* Interactive */
--interactive-primary: oklch(...);
--interactive-hover: oklch(...); /* more contrast than rest */
--interactive-active: oklch(...); /* even more contrast */
--interactive-focus: oklch(...);State overlays scale better than per-component state colors: define hover/press as two reusable overlay tokens (e.g. hover = the fill tone, press = the weak stroke tone, or translucent black/white steps) and apply them over ANY component surface. Every new component inherits correct states for free instead of minting its own hover/press variations.
---
Contrast
- Prefer APCA over WCAG 2 for perceptual accuracy
- Text contrast: minimum 4.5:1 (WCAG AA), prefer 7:1 (AAA)
- UI components: minimum 3:1
- Interactions increase contrast:
:hover/:active/:focushave more contrast than rest - Never rely on color alone — include text labels, icons, patterns
- Color-blind-friendly palettes for charts and data visualization
---
Application Patterns
Accent Application
- Primary actions (CTA buttons)
- Links (maintain accessibility)
- Key icons for recognition
- Section headers
- Hover states (introduce color on interaction)
- Focus rings matching brand
Background & Surfaces
- Tinted backgrounds (warm/cool neutrals, not pure gray)
- Colored sections to separate areas
- Subtle gradient backgrounds (intentional, not generic)
- Tinted cards for warmth
Borders & Accents
- Accent borders on cards (left/top)
- Colored underlines for active states
- Subtle colored dividers
- Colored focus indicators
Image Outlines
Give <img> a subtle 1px inset outline so images sit at the same depth as bordered/shadowed surfaces — and the outline color is non-negotiable:
- Light mode: pure black at low opacity —
rgba(0,0,0,0.1)(Tailwindoutline-black/10). - Dark mode: pure white —
rgba(255,255,255,0.1)(Tailwinddark:outline-white/10). - Never a tinted near-black/near-white from the palette (slate/zinc/neutral,
#0a0a0a,#f5f5f7) and never the accent/ink hue. A tinted outline picks up the surface underneath and reads as dirt on the image edge — this is the one place the hue-consistency rule above does NOT apply. - Use
outline+outline-offset: -1px(inset), notborder— keeps the image its intended size and adds nothing to layout.
img { outline: 1px solid rgba(0,0,0,0.1); outline-offset: -1px; }
/* dark: rgba(255,255,255,0.1) */---
Design Rules
- Limit accent color to one per view
- Use existing theme tokens before introducing new ones
- Never gradients unless explicitly requested
- Never glow effects as primary affordances
- Layered shadows: ambient + direct light
- Crisp edges: semi-transparent borders + shadows
Component Contracts
Anatomy-level rules for the workhorse components: buttons, menus, modals, search, cards, nav. Surface-level composition lives in the recipes; form fields live in forms.md; metric cards and tables in dashboard.md. This file is the layer below: what makes an individual component read as designed.
---
Buttons
Anatomy:
- Horizontal padding ≈ 2× vertical padding. Floors: 16px horizontal, 10px vertical. Equal padding on all sides makes a button read as a tile, not an action.
- Heights: 44-48px on touch, 32-40px acceptable on desktop-only surfaces. Why: pointer precision — a mouse hits a 32px target reliably; a thumb doesn't. When it breaks: any desktop surface that also ships to touch (most do) — then 44px is the floor everywhere. Hit-area expansion for smaller visuals: accessibility.md.
- Label 14-17px. Check the height/font pair for clean vertical centering: a 32px button with 17px text can't center on whole pixels — pick value pairs whose difference is even, or the label sits visibly off-center.
- Inline groups order left→right by importance, and every state (default, hover, pressed, loading, disabled) is distinguishable without being a costume change — one property shift per state transition is usually enough.
Icon placement carries meaning:
- Icon left of label = aids scanning, communicates the action type at a glance (add, edit, download).
- Icon right of label = consequence or destination ("Log out →", "Continue →", external-link marker).
- Don't decorate with an icon that repeats the label without adding either.
Depth: flat or one subtle shadow pair (layout.md). Gradient fills and inner shadows on buttons are expressive opt-ins for branded marketing surfaces — inside the product they read as 2015. Anti-slop rules apply.
Tiers and their contracts:
- One primary per context — two solid CTAs in one view is a tie, and ties stall. Secondary = outline at ≥3:1 contrast (a light-gray outline button is indistinguishable from disabled). Tertiary = text + underline — the underline is an accessibility requirement for color-blind recognition, not decoration.
- Avoid disabled buttons. They can't explain themselves, give nothing on press, and assistive tech often skips them. Alternatives, in order: keep enabled and validate on press with errors; remove the action and say why; lock icon + explanation on press. If one survives anyway, pair it with visible inline text saying what unlocks it.
- Icon weight matches text weight, icon size matches text size; if an icon overpowers its label, fix it with a lighter stroke, not a washed-out color.
Destructive actions — friction proportional to blast radius:
- Low (recoverable, undo exists): act immediately + undo toast. Friction here is rude.
- Medium (recoverable with effort): confirmation dialog with verb buttons + the affected items named and highlighted.
- High (irreversible, shared, or bulk): confirmation + explicit acknowledgment — type the resource name or tick a checkbox before the destructive button activates. A misclick must not be able to complete the flow.
- Destructive buttons get LOW visual prominence (tertiary/outline) in the normal UI — prominence belongs to the actions you want repeated. Inside the confirm dialog the destructive verb is the filled one, in the danger color.
Text Links
- The link affordance is exclusive. Underline + link color mean "this navigates" — nothing else gets that costume. A colored, underlined heading that isn't a link is a broken promise; an icon styled like a button that does nothing is the same lie. Decoration must never impersonate function.
- Inline links inside body text need the underline (color alone fails color-blind users); standalone links in chrome (nav, footers) may drop it when context already signals interactivity.
Icon Labels
- Prefer a visible text label next to icons. Only universally-learned glyphs survive alone (search, close, play); everything else is a guess that costs a tap to verify. Icon-only buttons require
aria-label+ tooltip as the floor (accessibility.md) — but visible labels beat both.
Menus & Dropdowns
- ≤5 options: don't use a dropdown. Radios or a visible segmented control — one less click, options scannable without interaction. The dropdown earns its collapse only past ~5 (6-10 still favors radios when vertical space allows; >10 wants type-ahead — full selection ladder in forms.md Field Layout).
- Long lists get a scroll container with an affordance: visible scrollbar plus the last visible item partially cut by the container edge or faded. A list that ends exactly at the container edge looks complete — users never scroll. Cutting an item mid-row is the strongest "there's more" signal available.
- Very long lists (countries, currencies, assignees) escalate to a searchable sheet/popover — type-ahead beats scrolling past ~20 items, especially on mobile.
- Multi-select: each selected option visibly marked in the list + a one-step "clear all". Nested options: signal children before expansion (chevron, count).
- Power-user menus show keyboard shortcuts inline, right-aligned, in muted mono — see the Command composition in recipe-dashboard.md.
- On low-contrast minimal surfaces, an open menu needs a hairline stroke and/or shadow pair to separate from the page — whitespace alone can't establish the overlay plane.
Modals
- A modal is for decisions that interrupt for good reason — destructive confirmations, flows that must not lose context. Minor errors and notices stay inline. If putting a task in a modal adds steps versus doing it in-flow, it doesn't belong in a modal.
- Three ways out, always: visible close button, an explicit cancel action, and overlay click (escape key too — accessibility.md focus-trap rules).
- Confirmation buttons are verbs, never Yes/No. "Delete project" / "Cancel" — the button restates the consequence. Yes/No forces re-reading the question, and a negatively-phrased question ("Don't you want to keep…?") plus Yes/No produces wrong clicks under time pressure.
- Multi-step flows inside a modal show progress (step count). If the flow grows past ~3 steps, question the modal — promote to a page.
Search
- Input ≥44px tall with a defined boundary (border or fill) — a search field users can't find is a navigation failure.
- Placeholder shows example queries or the dominant content type ("Search projects, people, docs…"), not the word "Search" the icon already says.
- Type-ahead suggestions as the user types; recent searches with individual remove + clear-all when the product is revisit-heavy.
- The no-results state is a fork, not a wall: show spelling-adjacent alternatives, popular queries, or a scoped-search escape ("search all workspaces instead"). A bare "no results found" ends the session — design it like any other empty state (state-design.md).
Cards (content cards)
Metric cards have their own contract in dashboard.md. For content/preview cards in grids:
- Variable-length content must not break grid rhythm. Either clamp text (line-clamp + optional tooltip for the full string) or set a shared min-height per row. Mixed card heights in one grid read as unfinished.
- Identical image aspect ratio across every card in the grid — one off-ratio image breaks the set.
- Card CTA at the bottom (the reading endpoint), with hover + active states on desktop and a full-card tap target on touch.
- Internal padding 16-24px; grid gap 16-40px scaled by [VISUAL_DENSITY]. Multi-column grids want short content; single-column (mobile) tolerates longer — the card is the whole viewport there.
Navigation (top bar)
- Logo top-left links home. Universal recovery convention — breaking it strands lost users.
- Active page visibly marked; hover states on every link.
- One CTA in the nav, visually distinct from both the plain links and the page's own primary CTA — three levels that must not tie.
- Sticky nav needs a plane separator once content scrolls under it: 1px hairline at low contrast, or backdrop blur (20-32px) over a 50-80% opacity fill. Without it the nav and content occupy the same visual plane and collide.
- Transparent/blurred navs must be contrast-checked against every background they'll scroll over — the hero may pass and the white section below fail. Provide a scrolled variant (solid fill) that activates past the hero.
- Overflow: collapse low-priority links into a "More" menu or push them to the footer; never shrink the type to fit.
---
Cross-Refs
forms.md inputs and validation · accessibility.md hit areas, focus, keyboard · dashboard.md metric cards, tables, toolbars · state-design.md empty/error states · motion.md state-transition timing
UX Copy & Writing
Voice, tone, reading level, terminology, inclusive language, locale handling, and tactical microcopy (errors, empty states, CTAs). Clear, human interface writing.
---
Voice vs Tone
Voice is constant. Tone varies. Voice is the product's personality — how it sounds whether you're reading onboarding, an error, or a paywall. Tone is how that voice adjusts to context: warmer in onboarding, restrained in success, explicit in destructive confirms. A product with a consistent voice but responsive tone reads as designed; a product with shifting voice reads as written by 12 people.
Voice Matrix
Pick three axes. Lock a position on each. Deviations are deliberate (brand campaigns, April Fools), never accidental.
| Axis | Endpoints | What to lock down |
|---|---|---|
| Formality | Formal ↔ Casual | Pick a position; document with one sample sentence at the chosen point and one at each extreme |
| Reverence | Reverent ↔ Irreverent | Reverent treats the user task as serious; irreverent allows wit and acknowledges absurdity. Pick one. |
| Expertise | Expert ↔ Beginner-friendly | Expert assumes vocabulary; beginner-friendly defines on first use. Mixed within a product is acceptable when the surfaces serve different audiences. |
Document the positions. "Casual-but-respectful, beginner-friendly, mildly irreverent" is a brief a writer can execute against. "Friendly" is not.
Tone by Context
Voice is fixed; tone moves along these contexts. Each row below assumes a default voice of "casual-professional, beginner-friendly, restrained."
| Context | Tone | Example |
|---|---|---|
| Onboarding | Warm + specific | "Let's get your first project in. Takes 60 seconds." |
| Error — user-recoverable | Honest + actionable | "We couldn't save your draft. Retry, or copy the error for support: abc-123." |
| Error — system outage | Honest + transparent | "Search is down. We're investigating. Your data is safe." |
| Success | Restrained | "Changes saved." — not "Amazing work!" |
| Destructive confirm | Explicit about consequences | "Delete 3 items? You'll have 30 days to restore them from Trash." |
| Empty state | Action-oriented | "No integrations yet. Connect Slack, GitHub, or Linear to get started." |
| Paywall / limit | Informational, not shaming | "You're at 5/5 free projects. Upgrade to create more." |
| Danger zone (settings) | Neutral + precise | "Deleting the workspace removes all 47 projects and invalidates all API keys." |
| Waiting / latency | Honest about duration | "Generating report. Usually 20-40 seconds." |
Never: exclamation points on errors. Emoji in enterprise contexts. Celebratory copy on routine saves.
---
Reading Level
Target ~8th-grade reading level (Flesch-Kincaid ~70+, grade ~8). Rules of thumb:
- Sentences ≤ 20 words.
- Paragraphs ≤ 3 sentences.
- Concrete nouns over abstractions ("invoice" beats "billing artifact").
- Active voice ("You signed in" beats "Sign-in was completed").
- One idea per sentence.
- Front-load. The key word or fact opens the sentence, heading, and notification — readers and screen-reader users decide whether to continue from the first words. Inverted pyramid for blocks: conclusion → support → background.
- Abbreviations are a tax. Spell terms out; if an acronym must repeat, define it on first use ("estimated arrival (ETA)") and stay consistent after.
Example rewrite:
| Before (grade 14) | After (grade 7) |
|---|---|
| "Upon successful authentication, users will be redirected to their personalized dashboard interface." | "You'll see your dashboard after you sign in." |
| "In the event that the operation is unsuccessful, please re-attempt the transmission." | "If it fails, try again." |
| "Configuration modifications will be persisted upon form submission." | "Your changes save when you click Save." |
Automate: hemingwayapp.com for spot checks; the `flesch` npm package for build-time linting. write-good (npm) flags weasel words, passive voice, lexical illusions. alex (npm) is an inclusive-language linter.
---
Terminology Consistency
Every product needs a glossary. Same concept → same word, everywhere.
Common pitfalls:
user/member/account/seat— pick one per concept and stick.project/workspace/board— pick one per level of hierarchy.delete/remove/archive/trash— these mean different things; don't alias.sign in/log in— both are standard; pick one and never mix.save/publish/update— same action under three names in three surfaces is a finding.
Red flag: in a review, grep the codebase for every verb on every button. If Save / Update / Apply appear on forms that do the same thing, file it.
---
Inclusive Language
Swap the loaded term for the neutral one. Keep the list tight — don't moralize, just fix.
| Replace | With |
|---|---|
| blacklist / whitelist | blocklist / allowlist |
| master / slave | primary / replica |
| sanity check | quick check |
| grandfathered | legacy |
| he / she (generic) | they |
| manhours / manpower | work hours / team capacity |
| man-in-the-middle | on-path attack |
| dummy value | placeholder value |
| first-class citizen | fully supported |
Don't soften to the point of vagueness — "on-path attack" is still precise; "bad thing happens in network" is not.
---
Locale-Aware Strings
Rules agents routinely ignore. Every one is shippable to production the day the product adds a second locale.
- Dates.
Intl.DateTimeFormat(locale, options). Never hardcodeMM/DD/YYYYunless the audience is exclusively US. Usenew Date().toLocaleDateString(locale, { dateStyle: 'medium' })for user-facing dates. - Numbers and currency.
Intl.NumberFormat(locale, { style: 'currency', currency }). Decimal and thousands separators differ:1,234.56(US) vs1.234,56(DE) vs1 234,56(FR). - Plurals.
Intl.PluralRules(locale). English has 2 forms; Russian has 3; Arabic has 6. Nevern === 1 ? "1 item" : n + " items"— breaks in Polish, Arabic, Welsh. - Length tolerance. German ≈ 1.3× English; Russian ≈ 1.2×; Chinese ≈ 0.5×. Don't fix widths to English copy. Test with
de-DEpseudo-locale. - RTL readiness. Use logical CSS properties (
margin-inline-start,padding-inline-end,border-inline-start), notmargin-left.dir="rtl"at the root should flip the UI without additional CSS. - Time zones. Store UTC, display in the user's zone. Never assume server time is user time. Use
Intl.DateTimeFormat(locale, { timeZone }). - Sorting.
Intl.Collator(locale)— alphabetical order is locale-dependent (Swedish sortsåafterz, German doesn't).
---
Core Principles (Tactical)
1. Be specific: "Enter email" not "Enter value" 2. Be concise: Cut unnecessary words (don't sacrifice clarity) 3. Be active: "Save changes" not "Changes will be saved" 4. Be human: "Something went wrong" not "System error encountered" 5. Be helpful: Tell users what to do, not just what happened 6. Be consistent: Same terms throughout — don't vary for variety
---
CTAs & Buttons
Verb + noun. Never Submit, OK, Click here, Continue.
| Generic | Specific |
|---|---|
| Submit | Create project |
| OK | Got it |
| Continue | Next: billing |
| Click here | Download invoice (PDF, 84KB) |
| Delete | Delete workspace |
- Describe the action specifically (verb + noun). Active voice.
- Destructive actions include the object: "Delete
project-alpha" — user confirms the thing, not a generic verb. Typing the name for irreversible operations is the gold standard. - Action reveals consequence. "Send invite" tells the user exactly what happens. "Submit form" does not.
- One primary per view section — solid for primary, outline/ghost for secondary.
- Disabled states explain why — not just grayed out, never rely on color alone.
---
Numbers & Control Labels
- Numerals, never spelled out: "245", not "two hundred forty-five" — numerals have distinct shapes and scan faster. Group thousands ("12,400"); huge numbers go hybrid ("1 billion", "2.4M") so nobody counts zeros.
- Form labels drop possessives: "Email", not "My email" or "Your email" — the possessive adds a word and an ambiguity (whose?).
- Button labels are verb + noun ("Save post", "Delete message"): readable out of context, which is exactly how screen-reader users jumping between controls hear them.
---
Error Messages
Three parts, in order: what happened + why + what to do.
- "Upload failed. The file is 62MB; the limit is 50MB. Try compressing it or splitting into parts."
- "We couldn't reach the server. Your internet looks offline. We'll retry automatically when you're back online."
- "Can't create the project. The name
betais already taken in this workspace. Try another name."
Missing pieces = bad error. "Something went wrong" has none. "Invalid input" has only the first.
Rules
- Explain what went wrong in plain language
- Suggest how to fix it
- Don't blame the user
- Include examples when helpful
- Link to help/support if applicable
Bad: "Error 403: Forbidden" → Good: "You don't have permission to view this page. Contact your admin for access." Bad: "Invalid input" → Good: "Email addresses need an @ symbol. Try: name@example.com"
---
Empty States
Bad: "No items" → Good: "No projects yet. Create your first project to get started."
- Explain why it's empty (if not obvious)
- Show next action clearly
- Make it welcoming, not a dead-end
---
Loading & Success States
Loading — Bad: "Loading..." (for 30+ seconds). Good: "Analyzing your data... this usually takes 30-60 seconds."
- Set expectations, explain what's happening, show progress, offer "Cancel".
Success — Bad: "Success". Good: "Settings saved! Your changes will take effect immediately."
- Confirm what happened, explain what's next, be brief but complete.
---
Confirmation Dialogs
Bad: "Are you sure?" → Good: "Delete 'Project Alpha'? This can't be undone."
- State the specific action
- Explain consequences (especially destructive)
- Clear button labels ("Delete project" not "Yes")
- Only for risky actions
---
Form Labels & Microcopy
- Clear, specific labels (not generic placeholders)
- Placeholders are examples, not labels — labels exist separately, placeholders end with
… - Explain why you're asking (when not obvious)
- Instructions before the field, not after
- Never use placeholders as the only labels
Tooltips — Bad: "This is the username field". Good: "Choose a username. You can change this later in Settings."
- Add value (don't repeat the label), answer "What is this?" or "Why?", inline help first, tooltips as last resort.
---
Formatting Rules
- Ellipsis for loading/follow-ups: "Loading…", "Rename…"
- Use `…` character not three dots
... - Curly quotes: " " not " "
- Non-breaking spaces:
10 MB,⌘ K - Numerals for counts: "8 deployments" not "eight deployments"
- Active voice: "Install the CLI" not "The CLI will be installed"
- Default to positive language: encourage, don't blame
- `<title>` reflects current context
---
Content & States
Every UI must design for the full range of content and states, not just the happy path:
- Design all states: empty, sparse, dense, error, loading, success
- Skeletons mirror final content exactly — prevent layout shift
- No dead ends — always offer next step or recovery
- Empty states guide toward action, not just "nothing here"
- Accessible names exist even when visuals omit labels
- Resilient to user-generated content: short, average, very long
---
Banned Patterns (Dark UX)
UX sins, not style choices.
- Confirmshaming. "No thanks, I don't care about security." Ship: "No thanks" as a plain link.
- Fake urgency. "Only 2 seats left!" when it's always 2. Ship: real counters or no counter.
- Fake scarcity. "87% of users upgraded" with no source. Ship: named, verifiable stats or omit.
- Roach motel. Easy signup, impossible cancellation. Ship: cancel in the same number of clicks as signup.
- Dark toggles. Opt-in checkboxes pre-checked for marketing emails. Ship: unchecked by default, unambiguous label.
- Bait-and-switch pricing. "$9/mo" that's actually "$9/mo, billed $108 annually, with a $29 setup fee." Ship: the total visible at decision time.
---
Never
- Use jargon without explanation
- Blame users
- Be vague without specifics
- Use passive voice unnecessarily
- Use humor for errors (be empathetic)
- Assume technical knowledge
- Vary terminology (pick one term, stick with it)
- Repeat information (headers restating intros)
- Use placeholders as the only labels
- Exclamation points on errors
- Celebratory copy on routine saves
---
Sources
- Nielsen, J. "How Users Read on the Web." Nielsen Norman Group.
- Mailchimp Content Style Guide — styleguide.mailchimp.com.
- Shopify Polaris — Content Guidelines.
- Atlassian Design System — Writing Style.
- Flesch, R. "A New Readability Yardstick." Journal of Applied Psychology, 1948.
- Unicode CLDR — locale data for dates, numbers, plurals.
Dashboard Patterns
Detailed guidance for building data-heavy dashboards that feel designed, not generated.
Layout Structure
A dashboard needs a sidebar + main content area. The sidebar is the navigation spine.
Sidebar navigation:
- Subtle background tint (e.g.,
background: #f8fafc/ Tailwind:bg-slate-50, orbackground: #030712/ Tailwind:bg-gray-950) — NOT full black unless Dark Premium style. A dark sidebar is a common AI pattern; prefer a subtle tint that complements the content area. - Muted nav text that brightens on hover/active. Active item gets accent background at low opacity (
background: oklch(var(--accent) / 0.1)/ Tailwind:bg-accent/10) + accent text. - Brand/logo at top, user profile at bottom.
aria-label="Main navigation"on<nav>,aria-current="page"on active item.overscroll-behavior: containon the sidebar if it scrolls independently.
Main content area:
- Filter/toolbar row at the top: ghost buttons for filters, active state uses accent bg at low opacity. Always include a date range selector for any time-series surface. Why: time-series data without an interactive range silently encodes the assumption that the default window is correct — when it isn't (and it usually isn't for power users), the dashboard becomes a screenshot. When it breaks: real-time monitoring surfaces with a fixed last-N-minutes window — the range is the affordance, not the picker.
- Content grid below filters: metric cards → charts → tables/lists.
- Minimum 3 different content types visible per viewport (e.g., metric cards + chart + table).
Metric Card Hierarchy
Never show 4+ identical metric cards. Differentiate the primary metric. Why: a uniform grid of equal-weight cards triggers the AI-template tell — variety signals editorial decision; uniformity signals defaulted-out. It also fails the squint test: the eye can't lock on a primary at first glance. How to differentiate: primary card gets accent tint, slightly larger number, optional sparkline; secondaries are neutral with smaller type. See Signal-to-Noise Hierarchy section below.
Primary metric card:
- Accent-tinted background (
background: oklch(var(--accent) / 0.05)/ Tailwind:bg-accent/5) with accent-colored number, OR solid accent background with white text. - Slightly larger number (36px vs 28px for others).
Secondary metric cards:
- White background, subtle border (
1px solid oklch(92% 0.005 250)). - Black number, secondary-color label.
All metric cards should include:
- Sparklines: 32px tall, polyline SVG, accent color with faded fill underneath.
- Change text: "+2,149 from last month" in
var(--text-tertiary). Never green arrows for positive, red for negative. Why: color implies judgment that may not match user context — a 30% increase in costs is "positive" by sign and "bad" by goal; the green arrow encodes the wrong story. Render the magnitude in neutral and let the user's interpretation supply meaning. When it breaks: financial trading surfaces where positive/negative is universally tied to goal (gains green, losses red is the domain convention) — match the user's domain, don't fight it. - Label: sentence case, 12-13px,
font-weight: 500, secondary color. - Value: 28-36px,
font-weight: 700,font-variant-numeric: tabular-nums,letter-spacing: -0.02em. - NO colored top/left borders. NO colored change text. NO arrow icons next to percentages.
Chart Type Decision Matrix
| Data story | Best chart | Why | Avoid |
|---|---|---|---|
| Trend over time | Area chart with gradient fill | Shows direction + volume | Vertical bar chart |
| Comparing categories | Horizontal bar chart | Labels are readable, easy to scan | Vertical bar with rotated labels |
| Comparing discrete values | Vertical bar chart | Natural for small sets (3-7 items) | Too many bars (>8) |
| Part-of-whole | Donut/ring chart (use sparingly) | Center text shows total | Pie chart — harder to compare |
| Inline trend in a card | Sparkline (32px polyline) | Minimal, contextual | Full chart crammed into a card |
| Conversion/funnel | Progressive bars with stage labels | Shows drop-off clearly | Donut chart |
| Never use | — | — | Pie charts, 3D charts of any kind |
Why never pie charts: Cleveland-McGill perceptual hierarchy ranks angle (pie) below position (bar). Humans compare bar lengths to within a few percent; pie wedges fail at 4+ slices. Why never 3D: depth occludes data points and foreshortens position-based comparison. No 2D chart benefits from a third dimension. When a two-segment donut is acceptable: a donut with center label for binary proportions (used vs free, paid vs free) — only one comparison to make.
Chart Styling
- Single accent hue at varying opacities for multi-series:
accent/100,accent/60,accent/30. Never rainbow colors. Why: hue does not encode ordering — readers can't rank red vs green by magnitude. Single-hue opacity ramps preserve perceptual ordering and remain colorblind-safe. Seedataviz.mdfor full palette guidance. - Gradient fill underneath area lines: line at full opacity, fill fades from ~15% at line to 0% at bottom.
- Label placement: axis labels in secondary text, 11-12px. Data point labels only on hover (tooltip), not permanently displayed.
- Grid lines: horizontal only, very subtle (
border-color: oklch(95% 0 0)/ Tailwind:border-gray-100). No vertical grid lines. - No chart chrome: skip legends when there's only one series. Put context in the card title instead.
Data Tables
Tables are the workhorse of dashboards. Make them earn their space.
- Row context: tiny avatars (24px), colored status dots (6px, not badges), flag icons, or proportion bars. Plain text tables feel like spreadsheets.
- Status indicators: small colored dots (6-8px) inline with text. "Active" with a green dot, not a green badge/pill. The dot conveys status; the text provides the label. Badges add visual noise and are an AI-slop pattern.
- Proportion bars: show relative values as width-proportional bars within cells. More visual than raw numbers.
- Row hover: subtle background highlight (
background: #f9fafb/ Tailwind:hover:bg-gray-50). - Headers: sentence case,
font-weight: 500, secondary color. Never uppercase table headers. Why: uppercase removes the lowercase letterforms that aid scan-pattern recognition; readers process uppercase ~13-20% slower than sentence case (Tinker 1969). Uppercase also reads as decorative-template, not data-functional. - Alignment: text left, numbers right, status center.
Filter & Toolbar Patterns
- Ghost buttons for all filter controls — never solid primary buttons in a toolbar. Why: a toolbar holds 5-15 tertiary actions (filter, sort, group, export, refresh). Each solid primary button competes for the user's primary-action attention budget; the dashboard's actual primary action (drill-in, edit, alert config) gets buried. Hick's Law applied to visual weight.
- Active filter state: accent background at low opacity + accent text, or subtle border change.
- Group related filters visually. Date range selector deserves more prominence (slightly larger or separated).
- "Reset filters" appears only when filters are active, as a text link — not a button.
Content Density
Dashboards value density over breathing room (opposite of landing pages).
- Metric cards: tight padding (16-20px), not the generous 32-48px of landing page cards.
- Charts: minimize whitespace around the chart area. The data should fill the container.
- Tables: compact row height (40-48px). Dense but scannable.
- A dashboard should show enough data that a user can make a decision without scrolling. If the first viewport is just 4 large metric cards, you've wasted space.
- Aim for: 3-4 metric cards + at least one chart + start of a table/list — all above the fold.
---
Signal-to-Noise Hierarchy
The most common AI-generated dashboard failure isn't any single bad choice — it's that every KPI, every chart, every card looks equally important. The user doesn't know where to look. Signal-to-noise hierarchy is the fix.
Tiers
Tier 1 — The hero metric. One per dashboard. The number that answers "am I winning today?". Largest visual weight. Accent color is allowed here, and here only. Trend indicator adjacent (up/down + delta). Context micro-text below: "vs last 7 days" or "of monthly quota."
Tier 2 — Supporting metrics (2-4). The breakdown of the hero — if the hero is revenue, supports are revenue by channel, by region, by segment. Neutral colors. Smaller type. Grouped visually with the hero (shared container, aligned baseline).
Tier 3 — Context (0-3 charts). Trends, time series, comparisons. Chart type matches data shape (see dataviz.md and the Chart Type Decision Matrix above). Sparklines for micro-context inside cards. No large charts competing with the hero's visual weight.
Tier 4 — Deep-dive. Tables, filters, raw data. Accessible but not crowding the viewport. Often collapsed by default, or lives on a separate tab / route. The user drills in when they need it.
The anti-pattern: "grid of 8 KPI cards with equal weight"
Every card the same size, same color, same layout → the user's eye has nowhere to land → the user scrolls looking for meaning → the user leaves. This is the single most recognizable AI-generated dashboard shape.
The rule
One hero. 2-4 supports. Context behind. Deep-dive one click away. Test by squinting at the dashboard: your eye should land on ONE thing first. If it lands everywhere, the hierarchy is broken.
Ranking decisions to document per dashboard
Before building, answer these:
- What's the hero metric for this surface? Different for exec view (revenue) vs operator view (tickets open) vs customer view (usage vs limit). See
personas.mdto ground this in concrete archetypes. - What supporting metrics contextualize the hero? The 2-4 that make the hero actionable, not the 8 you could show.
- What's deep-dive, not surface? Everything the user needs once a week, not once a minute.
- Who's the user and what decision are they making in the next 60 seconds? If you can't name the decision, the dashboard has no job.
Cross-references
dataviz.md— chart selection by data shape; Cleveland-McGill perceptual hierarchy (position-on-common-scale is most accurate — use it for the hero).heuristics.md— Cleveland-McGill PASS/FAIL, Hick's Law for filter/nav choices.personas.md— concrete user archetypes to ground "who's the user" in reality, not abstraction.
Dataviz — Perceptual Accuracy
Most AI-generated charts are rainbow 3-D pies on grid backgrounds with shouting legends. That's not dataviz — that's decoration. Dataviz is a science with names attached: Cleveland, McGill, Tufte, Bostock, Brewer. This file is the perceptual layer — dashboard.md handles dashboard chrome (sidebar, metric cards, toolbar); this handles what actually encodes information accurately.
---
Cleveland-McGill Perceptual Hierarchy
Source: Cleveland, W. S. & McGill, R. (1984). "Graphical Perception: Theory, Experimentation, and Application to the Development of Graphical Methods." JASA.
Humans decode visual encodings with predictable, measurable accuracy. Ordered from most → least accurate:
1. Position on a common scale — e.g., dot plot, aligned bar chart. Highest precision. 2. Position on unaligned scales — e.g., small multiples with different baselines. 3. Length — e.g., bar chart, progress bar. 4. Angle / slope — e.g., line chart slopes, pie wedges. 5. Area — e.g., bubble chart, treemap. 6. Volume — e.g., 3-D bar (and yes, this is already worse than area). 7. Color saturation / lightness — e.g., heatmap. 8. Color hue — e.g., categorical colors. Lowest precision for quantities.
Implication: if a numeric comparison is load-bearing, prefer bar over pie, line over stacked area, dot plot over heatmap. Color hue is for categories, not quantities.
---
Chart Selection Matrix
Match the data shape and analytical question to a chart. Fallbacks listed when the default isn't possible.
| Data shape + question | Default | Fallback | Avoid |
|---|---|---|---|
| Time series, one metric | Line | Area (sparingly, single series) | Vertical bars per day |
| Time series, categorical breakdown, totals | Stacked bar or stacked area | — | 100% stacked (hides totals) |
| Time series, categorical breakdown, individual trends | Small multiples (one line per panel) | Multi-line (≤ 4 series) | Spaghetti line chart (> 5 series) |
| Categorical, one metric | Horizontal bar (sorted) | Dot plot | Pie, unless n ≤ 3 and parts-of-whole is the story |
| Parts of a whole | Stacked bar (100%) with labels | Donut (n ≤ 5) | Pie with n > 5; donut without center total |
| Distribution of one variable | Histogram | Density / violin | Mean-only bar |
| Distribution across groups | Box plot or violin | Strip plot with jitter | Mean + error bar alone |
| Correlation of two variables | Scatter + trend line | Hex-bin (high n) | A correlation coefficient printed as a single number |
| Ranking | Horizontal bar sorted desc | Dot plot with reference line | Vertical bar with rotated labels |
| Geographic (regional) | Choropleth, per-capita normalized | Symbol map | Raw-count choropleth (shows population, not signal) |
| Flow / transition | Sankey (sparingly) | Cohort table, small multiples | Chord diagram for > 8 categories |
| Hierarchy | Tree / indented list | Treemap (area ≫ precision) | Sunburst for > 3 levels |
Rule of thumb: if you can't name the question the chart answers, remove it.
---
Color for Data
Three families. Choose by the data's semantics, not by taste.
Sequential — one direction (low → high)
For ordinal or continuous magnitude: revenue, count, score. Use perceptually uniform palettes.
- viridis, cividis (perceptually uniform sequential — available in every charting stack) — colorblind-safe, print-safe.
- YlGnBu, YlOrRd (ColorBrewer) — classic sequential, works on light backgrounds.
Never use a rainbow (hue-only) palette for sequential data. Hue is not ordered — the eye cannot rank red vs green by magnitude. Ordered magnitude needs a luminance ramp (viridis, cividis, YlOrRd). Hue-only is also colorblind-hostile: red-green deficiency affects approximately 8% of men and 0.5% of women (Birch 2012; Sharpe et al. 1999) — exactly the mid-spectrum of a rainbow.
Diverging — centered on a meaningful midpoint
For values with a neutral zero: surplus/deficit, above/below average, net promoter score.
- RdBu, PiYG, BrBG (ColorBrewer).
- The midpoint must be semantic, not arbitrary. Don't use diverging for "low to high" — that's sequential.
Categorical — nominal classes
For discrete groups with no order: regions, departments, products.
- Okabe-Ito (Okabe & Ito 2008, colorblind-safe, 8 hues). Values, in series order:
#E69F00 orange, #56B4E9 sky blue, #009E73 bluish green, #F0E442 yellow, #0072B2 blue, #D55E00 vermillion, #CC79A7 reddish purple, #000000 black.
- Cap at 7 hues. After that, hues become indistinguishable. Options: (a) group remainder into "Other", (b) repeat hues with patterns, (c) direct-label each series and drop the legend.
Never-defaults
- Pie chart with > 5 slices — Cleveland-McGill: angle is the fourth-lowest encoding in the perceptual hierarchy; humans cannot accurately compare more than 4–5 angular wedges. Use a horizontal bar chart sorted by magnitude instead.
- Rainbow / HSL-rotation palette for sequential or ordinal data — hue carries no perceptual ordering; the eye cannot rank hues by magnitude without trial-and-error. Diverging data needs a two-direction luminance ramp (RdBu, PiYG); sequential data needs a single-direction luminance ramp (viridis, YlOrRd). Hue-only is also colorblind-hostile: red-green deficiency affects approximately 8% of men and 0.5% of women (Birch 2012; Sharpe et al. 1999). Test every palette with a colorblind simulator (Color Oracle, Coblis) — non-negotiable for status colors.
- Red and green as the only signal — pair color with shape, icon, or text label; color alone fails for red-green deficient users (same citation above).
- 3-D charts of any kind — depth occludes data points; foreshortening distorts position-based comparison (Cleveland-McGill: volume is second-lowest in the hierarchy). No 2-D chart benefits from a third dimension.
- Chart library defaults without a palette override — built-in palettes (Chart.js, Recharts) are designed for demos, not perceptual accuracy; always override with a named, colorblind-safe palette.
- Default grid at every tick — Tufte's data-ink ratio: a dense grid is chartjunk. Use sparse grid lines (every 2nd or 3rd tick), or baseline + target lines only.
---
Tufte Principles, Distilled
Source: Tufte, E. R. (1983, 2001). The Visual Display of Quantitative Information.
- Data-ink ratio — maximize ink spent on data, minimize ink spent on chrome. Every gridline, tick, border, legend box must earn its space.
- No chartjunk — no 3-D, no gradients as decoration, no drop shadows on bars, no textures.
- Small multiples over animated dashboards — the eye compares adjacent panels faster than it compares across a timeline.
- Annotation beats legend — label the line, not a color swatch 200px away (see "Direct labeling" below).
- Bar charts start at zero. Length encodes magnitude; truncating the axis lies.
- Line charts do NOT have to start at zero. Position encodes change; a zero baseline can obscure the signal. Use judgment — when the story is "rose from 98 to 102", starting at zero hides it.
---
Direct Labeling — the Quiet Superpower
A legend forces: read color, look 200px away, match, remember, come back. Direct labels collapse that into one saccade.
// Bad — legend + color lookup + cognitive hop
<LineChart data={data}>
<Line dataKey="revenue" stroke="#1f77b4" />
<Line dataKey="costs" stroke="#ff7f0e" />
<Legend />
</LineChart>
// Good — label at the end of each line, legend gone
<LineChart data={data}>
<Line dataKey="revenue" stroke="#1f77b4">
<LabelList dataKey="revenue" position="right" />
</Line>
<Line dataKey="costs" stroke="#ff7f0e">
<LabelList dataKey="costs" position="right" />
</Line>
</LineChart>Works for: bar charts (label inside or at end of bar), scatter (label the outlier dots), small multiples (title per panel replaces legend).
---
Small Multiples
Use when comparing the same metric across a dimension — 8 regions, 6 products, 12 months. The eye compares adjacent panels instantly; a single overlaid chart with 8 lines is a mess.
Layout rules:
- Consistent axes across panels — same x-scale, same y-scale. If scales differ, the comparison breaks.
- Consistent chart type — all lines, all bars; never mix.
- One title explaining the one thing that varies ("Revenue by region, Jan-Dec"), plus a small subtitle per panel naming that panel's dimension value.
- 3-6 columns typically. 10+ panels = let it wrap; keep row heights equal.
- Sort panels by a meaningful order (magnitude, geography, alphabetical) — not by insertion order.
---
Animated Transitions for Dataset Changes
Animate to reveal change, not for decoration. The D3 enter / update / exit pattern — now standard in Recharts, VisX, Framer Motion, and Observable Plot:
- Enter — new data points fade in and slide to position (200-300ms,
ease-out). - Update — existing points tween to new values (300-500ms,
ease-in-out). The tween is the insight — the user's eye tracks which point moved where. - Exit — removed points fade out and collapse (150-200ms,
ease-in).
Rules:
- Tween values, not colors — category color stays stable so the eye can track a specific series.
- Respect
prefers-reduced-motion— collapse to instant transitions. - Never animate on mount just for the entrance. A dashboard loading is not a moment for choreography.
See references/motion.md for easing curves and the duration scale.
---
Anti-Slop Checklist (Dataviz)
Ten items a reviewer will flag immediately. Fix before shipping.
- [ ] 3-D charts of any kind. Zero exceptions.
- [ ] Pie chart with > 5 slices, or any pie where parts-of-whole isn't the story.
- [ ] Rainbow / HSL-rotation palette for sequential or numeric data.
- [ ] Red and green as the only differentiator (color-blind failure — add shape or label).
- [ ] Truncated y-axis on a bar chart (bars lie about magnitude).
- [ ] Dense gridlines at every tick. Prefer sparse grid (every 2nd or 3rd), or baseline + target lines only.
- [ ] Axis labels in scientific notation (
1.2e6) when1.2Mor1,200,000is readable. - [ ] Missing units on axes ("Revenue" vs "Revenue (USD, thousands)").
- [ ] Legend with > 7 entries without direct labeling.
- [ ] Choropleth of raw counts instead of per-capita / rate normalization.
---
When ui-craft-detect Flags dataviz/* Rules
The detector rules map directly onto this file:
dataviz/no-pie-gt-5→ § Cleveland-McGill + § Chart Selection Matrixdataviz/no-3d→ § Anti-Slop Checklistdataviz/sequential-not-rainbow→ § Color for Data → Sequentialdataviz/direct-label-over-legend→ § Direct Labelingdataviz/bar-starts-at-zero→ § Tufte Principles
Cross-reference when triaging a flagged chart.
---
Sources
- Cleveland, W. S. & McGill, R. (1984). "Graphical Perception." JASA.
- Tufte, E. R. (2001). The Visual Display of Quantitative Information, 2nd ed.
- Bostock, M. et al. (2011). "D3: Data-Driven Documents." IEEE TVCG.
- Brewer, C. A. ColorBrewer 2.0 — colorbrewer2.org.
- Okabe, M. & Ito, K. (2008). "Color Universal Design."
- Wilke, C. O. (2019). Fundamentals of Data Visualization. O'Reilly.
- Birch, J. (2012). "Worldwide prevalence of red-green color deficiency." Journal of the Optical Society of America A.
- Sharpe, L. T. et al. (1999). "A new and popular explanation of colour blindness." In Colour Vision: From Genes to Perception. Cambridge University Press.
Finish Bar
Ten finishing passes, each with measurable criteria. Run before shipping. Gated to CRAFT_LEVEL ≥ 8 or /finalize. The point is not more rules — each pass surfaces a class of failure the others can't catch. Passes share no blind spots by design.
---
How to Run the Bar
Run passes in order. Earlier passes (hierarchy, type system, surface stack) are foundational — changing them after later passes would invalidate findings downstream. Sequence is not arbitrary.
For each pass: collect findings, assign severity (Critical / Major / Minor), then fix or defer with a written reason. "Defer" without a reason is not a defer, it's a skip.
Done means each pass returns zero Critical and zero Major findings. Minor findings are explicitly accepted — write them down and ship anyway, or address them. Unwritten Minor findings are ignored findings.
Time budget: one screen with a single primary action takes 15–30 minutes. A full multi-screen flow takes hours. Don't try to run the bar on the whole app at once — run it on a vertical slice, ship, repeat.
The bar is opinionated about what matters. When a pass conflicts with a recorded project decision (e.g., the project deliberately uses dense data tables that violate Pass 4's spacing rhythm), the pass yields. Cite the principle in brief.md that overrides. If no brief principle covers it, the conflict is a finding, not a defer.
---
Pass 1 — Hierarchy
Goal: the surface resolves to one primary thing on first glance. The rest reads as supporting.
Criteria:
- Squint test passes: blur the screen or squint for 200ms. One element dominates. (Perceptual basis in
layout.md— Squint Test section.) - Primary / Secondary / Tertiary / Quaternary named explicitly for this surface before touching pixels. Write them down.
- Adjacent hierarchy levels differ by ≥1.5× in at least one signal: size, weight, contrast, surface area, or position.
- One focal point per viewport. Two elements at the same visual weight means one is wrong.
- Navigation, footers, and sidebars sit below content in perceived weight — they are infrastructure, not content.
How to verify: apply filter: blur(8px) in DevTools or squint physically. If the layout reads as a flat field of similar shapes, hierarchy failed. If two things fight for dominance, name which one wins and why; the loser gets demoted.
When it doesn't apply: purely transactional confirmations (a single action + summary) legitimately collapse to two levels — the action versus everything else. That's correct.
---
Pass 2 — Type System
Goal: typography is a system, not a collection of styles that happen to coexist.
Criteria:
- ≤3 font weights in the visible viewport. More signals indecision, not richness.
- Tabular numerals on every cell, badge, or display element containing numerical data (
font-variant-numeric: tabular-nums). - OpenType features active where the font supports them: discretionary ligatures off in UI, contextual alternates on for serif headlines, kerning always on.
- Prose body line-length: 50–75 characters. UI labels, badges, and tables are exempt.
- Letter-spacing scoped per role: Latin sans-serif display tightens at large sizes; small utility labels widen. Never adjust CJK or Arabic spacing — those scripts have their own optical metrics.
- One body face, optionally one display face, optionally one mono. No fourth face.
How to verify: grep rendered font-weight declarations on the surface — count distinct values. Inspect numbers in DevTools computed styles for font-variant-numeric. Measure a prose container by pasting its text into a character-count tool.
When it doesn't apply: single-screen experiences with no prose (a confirmation dialog, a product card) skip the line-length criterion. The weight and numeral criteria still apply.
---
Pass 3 — Surface Stack
Goal: at least three distinguishable elevation levels, both modes intentional.
Criteria:
- Canvas, raised, and overlay surfaces are visually distinct: ≥2% luminance delta in light mode, ≥4% in dark mode.
- Semantic naming in tokens:
--surface-canvas,--surface-raised,--surface-overlay(or equivalent). No unnamed magic values. - Dark mode surfaces are not inverted light primitives. Canvas in dark sits at a near-black with a hue tint — not
#000000. Raised surfaces add luminance, not just reduce it. color-schemedeclared so native form controls follow the mode.- Shadows fall back to border tints in dark mode where shadow-on-dark loses effect. A 10% opacity drop shadow on a near-black background is invisible — replace it.
How to verify: render side-by-side in both modes. Toggle rapidly. Dark should not look like a photo-inverted version of the light surface — it should look like a deliberate reinterpretation. Cross-ref the intentional-dark test in tokens.md.
When it doesn't apply: single-mode embedded UIs (a terminal panel, an e-ink reader context) where dark mode is not a product requirement.
---
Pass 4 — Spacing Rhythm
Goal: within < between < section, at every nesting level without exception.
Criteria:
- Every spacing value comes from the token scale. No arbitrary px values. (
layout.md— Spacing Rhythm section.) - The rhythm invariant holds at every nesting level: label 4px above its input, inputs 16px apart within the form, form 48px from the next section. Check three nesting levels minimum.
- Section breaks are ≥2× the inter-block spacing. If they are not, sections blur into blocks.
- Optical adjustments noted explicitly: icon vertical nudge to optical center, modal vertical offset for visual weight. Document these as intentional, not accidents.
How to verify: scroll the surface slowly. At each nested group, check that the surrounding space is visibly larger than the internal space. If you cannot tell which level you are in from spacing alone, the rhythm broke.
When it doesn't apply: dense data tables and command palettes deliberately compress. The proportion still holds — the absolute values shrink. Never collapse two adjacent levels to the same value regardless of density.
---
Pass 5 — Iconography
Goal: one icon family, stroke weight matched to type, geometry coherent across the surface.
Criteria:
- Single icon family throughout the surface. No mixing families from different systems.
- Icon stroke weight visually matches body type weight: 1.5px stroke for
font-weight: 400, 2px for 500. Mismatched weights read as elements from different UIs. - Icon-to-type size ratio is consistent: if a 24px icon appears beside a 16px label, that ratio holds throughout. A 20px icon beside a 16px label elsewhere is a finding.
- Container shape is one of: circle, rounded square, squircle. Chosen once, held throughout. No surface where some icons have circle containers and others have rounded squares.
- No non-icon characters standing in for designed icons anywhere outside user-generated content contexts.
How to verify: collect every icon instance on the surface in a contact sheet (screenshot + arrange). They should look like siblings from one family. Any that don't match immediately is a finding. Cross-ref inspiration.md — "What Mature Interfaces Never Do" section.
When it doesn't apply: emoji as user content (chat reactions, message-thread rosters) are content, not icons — they are exempt from family and stroke criteria.
---
Pass 6 — State Coverage
Goal: every state designed, not defaulted or missing.
Criteria — all eight states must have explicit designs:
- Idle — the resting state is intentional, not just the absence of everything else.
- Loading — skeleton matches the final layout geometry; shown after ~200ms to avoid flash on fast connections.
- Empty — one line explaining why it's empty + one primary action. Never "No items found" with no next step.
- Error — inline, actionable: what failed + what the user does next. Never a modal for a recoverable field error.
- Success — visible beyond color change: icon swap, position shift, or explicit confirmation text.
- Partial — unknown values rendered as em-dash; never
null,N/A, or0for a value that isn't actually zero. - Conflict — concurrent-edit or stale-data scenarios handled with explicit UI, not silent overwrites.
- Offline — skeleton persists past the loading timeout with an affordance that explains the surface is stale.
How to verify: write the eight state names on paper. For each interactive surface, mark which states have explicit designs. Unmarked states are Critical findings. Cross-ref state-design.md.
When it doesn't apply: read-only static surfaces (an embedded widget, a printed report) genuinely have only idle and loading. Conflict and offline states are not applicable.
---
Pass 7 — Motion Tuning
Goal: every transition is purposeful, sub-400ms for UI, and motion-gap-clean.
Criteria:
- UI transitions use the duration scale's transition band: 100–400ms. Nothing outside this range for interactive state changes.
- Easings have perceptual basis: no
linearfor spatial motion, no symmetricease-in-outon hover — spatial motion should ease out. (Cross-refmotion.md— Duration and Easing Scale.) - Motion-gap audit clean: no interactive state changes that snap without a transition where the change is visually significant (appearance, disappearance, repositioning).
- Reduced-motion contract honored:
prefers-reduced-motion: reducecollapses durations to ≤80ms or removes entrance animations entirely. The surface must be fully usable without motion. - Custom curves where the surface demands character. The four named CSS easing keywords are a starting point, not a destination.
How to verify: trigger every interactive state in sequence and watch the transitions. Anything that snaps where a transition is expected is a Pass 7 Critical. Verify prefers-reduced-motion in DevTools by emulating the media feature.
When it doesn't apply: static surfaces with no state changes have no motion to audit. Skip the motion-gap pass, but the reduced-motion contract still applies if any CSS animation exists.
---
Pass 8 — Microcopy Voice
Goal: every string has voice, specificity, and a single author.
Criteria:
- Verbs consistent across primary actions throughout the surface. Pick one form and hold it.
- No placeholder copy in production paths: no lorem ipsum, no generic names, no TODO comments, no XXX strings.
- No generic CTAs — every call to action names the specific outcome of the action.
- Error messages name what operation failed and offer a concrete next step. "Something went wrong" is not an error message.
- Empty states contain exactly one primary action — not three suggestions, not a paragraph, not a link list.
- Voice axes locked per the brief: formality, expertise level, and relationship position documented in
brief.md. All strings on this surface should sound like the same product.
How to verify: extract every visible string from the surface. Read each one out of context — stripped of layout and visual hierarchy. Each should still communicate clearly and sound like the same product. Cross-ref copy.md voice matrix and brief.md.
When it doesn't apply: legal text (terms, privacy notices, regulatory copy) is locked to compliance language and exempt from product voice criteria.
---
Pass 9 — Pixel Honesty
Goal: small details match the resolution intent of the design.
Criteria:
- Borders use sub-pixel approaches where a hairline is intended:
border: 1px solid color-mix(in oklch, var(--text-primary) 8%, transparent)reads as a separator; a solidneutral-200border reads as a structural edge. Use the right one. - Shadow stacks use 2–3 layers for depth perception. A single shadow at one blur value looks flat and unconvincing.
- Corner radii vary by element role: buttons, cards, and inputs each have a distinct radius. Uniform radius on every element is an AI-template tell — the single most recognizable signal of generated UI. (Cross-ref
inspiration.md— Reference Token Values section.) - Overflow handled on every text container: truncation includes a
titletooltip, notext-overflow: ellipsiswithout amax-widthset. - Icon containers use squircles where the design language calls for it — circle and rounded square are not interchangeable.
How to verify: zoom to 200%+ and inspect details. Anything that "looks slightly off" at that zoom is a finding worth naming. The test is not whether it looks perfect at 100% — it's whether the details hold up under scrutiny.
When it doesn't apply: utility surfaces (internal tooling, admin dashboards) where the brief explicitly records that polish is deferred in favor of completeness. The decision must be in brief.md, not assumed.
---
Pass 10 — Data Formatting
Goal: every number, date, and currency communicates the right thing at the right precision.
Criteria:
- Tabular numerals on all data display: cells, KPIs, badges, inline metrics (
font-variant-numeric: tabular-nums). - Counts abbreviated where the absolute value isn't the point:
1.2k,3.4M,12.5h. Unabbreviated where the exact value matters. - Relative time where the user cares about recency (
2m ago,yesterday). Absolute time where provenance matters (Mar 4, 14:32 UTC). Never use one format everywhere — they answer different questions. - Currency formatted to locale. Negative values distinguished by at least two signals: sign + color, or sign + parentheses for accounting contexts.
- Precision matches the decision granularity the user needs. A percentage to five decimal places is noise. A stock price rounded to the dollar is misleading. Match the precision to the question the number answers.
How to verify: inspect every numeric value on the surface. For each: what question does this number answer, and is the format right for that question? A count that could be 1,248 but shows 1.2k when the user needs the exact number is a finding. Cross-ref dataviz.md for number formatting in chart contexts.
When it doesn't apply: integer-only surfaces (step counts, indices, item counts) skip currency and decimal precision criteria. Tabular numerals still apply.
---
How Findings Map to Severity
Critical — block ship:
- Pass 1: no discernible hierarchy (flat field of equal-weight elements)
- Pass 6: states missing explicit designs (loading, empty, or error undefined)
- Pass 8: placeholder copy in production paths (lorem ipsum, TODO strings, generic names)
Major — fix before ship when time allows; document if not:
- Pass 3: surface stack incomplete or dark mode is an inverted light layer
- Pass 7: motion-gap failures on visible state changes;
prefers-reduced-motioncontract not honored - Pass 9: uniform border-radius across all element types
- Pass 2: proportional numerals on tabular data (misaligned columns, impossible to compare)
Minor — polish; ship-okay if explicitly accepted:
- Pass 2: single weight-count violation (four weights, not three)
- Pass 5: icon stroke weight off by a half-unit
- Pass 10: currency format missing locale
- Pass 4: one spacing value that skips a token (an arbitrary 20px in an otherwise clean scale)
---
When to Skip the Bar
- Internal tools where
brief.mdexplicitly defers polish to product completeness. The deferral must be written, not assumed. - Throwaway prototypes that will be discarded after a user test. If there is no brief, there is no bar.
- Single-screen confirmation flows where only 2–3 passes have anything to evaluate. Run those passes only, note the others as N/A.
The bar is a tool, not a ritual. Skipping it without recording why is the only wrong answer.
---
Cross-References
brief.md— which passes can be deferred and which are non-negotiable for this product; voice axes for Pass 8.tokens.md— Passes 3, 4, 7, and 9 require token discipline; intentional-dark test in Pass 3.layout.md— Pass 1 (hierarchy and squint test) and Pass 4 (spacing rhythm) primary references.typography.md— Pass 2 (type system) primary reference.motion.md— Pass 7 (motion tuning) primary reference; duration and easing scale.state-design.md— Pass 6 (state coverage) primary reference; state lattice.copy.md— Pass 8 (microcopy) primary reference; voice matrix, error copy, empty states.dataviz.md— Pass 10 (data formatting) primary reference for numeric display in chart contexts.inspiration.md— Pass 5 (iconography) and Pass 9 (pixel honesty); "What Mature Interfaces Never Do" and Reference Token Values sections.accessibility.md— every pass assumes a11y is satisfied at the floor. The finish bar is the ceiling, not the floor. Runaccessibility.mdfirst.
Project & User Memory — Self-Correction
A portable, file-based memory system so the skill remembers conventions and corrections — at two reaches — without ever weakening its quality floor. Modeled on how persistent agent memory works (engram / MemGPT-style tiers / mem0), but implemented as plain files so it works identically across every harness with no database, MCP, or network.
Two stores, two reaches
| Store | Location | Holds | Travels |
|---|---|---|---|
| Project | .ui-craft/memory/ (in the repo, beside .ui-craft/brief.md) | Conventions and corrections specific to this codebase | Commit it → the team shares it |
| User/global | ~/.ui-craft/memory/ (user home) | Things the user wants applied across all their projects | Follows the user, every repo |
The user told you something they want to replicate everywhere → it belongs in the global store. Something true only of this codebase → project store. When unsure, see Choosing the reach below.
The precedence ladder
Every decision resolves top-down. A higher tier always wins.
1. HARD FLOOR — never overridden by any memory
a11y (keyboard, focus-visible, APCA contrast, reduced-motion),
correctness, the Critical anti-slop tells.
2. PROJECT MEMORY (.ui-craft/memory/) ← most specific
3. USER/GLOBAL MEMORY (~/.ui-craft/memory/) ← the user's cross-project rules
4. SKILL DEFAULTS (references/* + Knobs)Specific beats general: a project memory overrides a global one (a per-repo correction wins over the user's usual taste). Both override skill defaults. Nothing overrides the floor — if a correction would breach it (e.g. "remove focus rings"), apply the closest compliant interpretation and say so in one line. Never silently obey, never silently refuse.
Store layout — tiered, like real memory systems
Each store is a directory, not one flat file. This gives two tiers — a cheap always-loaded index (core memory) and on-demand full records (archival memory):
.ui-craft/memory/ (or ~/.ui-craft/memory/)
INDEX.md ← TIER 1 (core): one line per memory, ALWAYS loaded at Discovery
profile.md ← TIER 1 (core): stack/tokens/style — always loaded (project store only)
2026-06-23-no-gradient-hero.md ← TIER 2 (archival): one file = one atomic memory, read on demand
2026-06-20-press-scale.md
...INDEX.md — the recall layer. One line per memory: its id, a one-line hook, and tags. Loaded every session so the skill knows what it knows cheaply; it reads the full memory file only when a hook matches the current task. This is the portable stand-in for semantic search — at project scale, hooks + the model's judgment match as well as a vector store, with zero infra.
# ui-craft memory index
- no-gradient-hero → 2026-06-23-no-gradient-hero.md — never gradients on hero here · #color #hero #brand
- press-scale → 2026-06-20-press-scale.md — cap button press at scale(0.97) · #motion #buttonprofile.md — autodetected project shape (stack, tokens, brand hue, font, style). Always loaded; refreshed when the stack changes. (Project store only — the global store has no single project to profile.)
Atomic memory file — one record, with frontmatter:
---
id: no-gradient-hero
type: correction # profile | convention | preference | correction
scope: surface:hero # all (default) | surface:<name> | stack:<name>
status: active # active | superseded
date: 2026-06-23
supersedes: [] # ids this replaces
tags: [color, hero, brand]
---
Rejected a gradient hero background.
**Why:** brand reads enterprise/restrained — flashy undercuts trust.
**Apply:** never propose gradient backgrounds on hero/landing surfaces in this project.Field meaning: type (profile = project shape, convention = established pattern, preference = taste, correction = a fix the user made). scope narrows within a store; the folder decides reach. status + supersedes handle conflicts. tags feed the index hooks.
The Why is mandatory — it's what lets a memory generalize instead of pattern-matching one literal case. The Apply line is the operative rule the next build follows without the original context.
Read — at Discovery
In Discovery Step 1, after the brief, load memory both stores (global first, then project so project can override):
1. Read ~/.ui-craft/memory/INDEX.md if present, then .ui-craft/memory/INDEX.md + profile.md. 2. Apply profile as known facts — skip questions it already answers. 3. For the current task, pull the full memory files whose index hooks/tags match the surface or decision at hand. Treat each active entry as a binding constraint at its tier. 4. On any conflict, resolve by the ladder (project > global > defaults; floor over all). 5. Absent stores → behave exactly as today. Memory is additive, never required.
Write — when corrected
Append a memory when the user signals a correction or a durable preference. Triggers (any language):
- Rejects/dislikes output: "no así", "no me gusta", "that's not what I want", "undo that".
- States a standing rule: "siempre haz X", "always use Y", "never Z".
- Reverses a non-default choice and the reversal reads as a preference, not a one-off.
Do not write for a one-off tweak with no generalizable reason, exploratory back-and-forth that doesn't land, or anything an existing memory already covers (update that one instead).
Choosing the reach (project vs global)
- Global signals → write to
~/.ui-craft/memory/: "in all my projects", "en todos mis proyectos", "siempre que trabajes conmigo", "as a rule for me", "I always like…". The preference is about the user's taste, not this brand. - Project signals → write to
.ui-craft/memory/: "here", "this project", "en este proyecto", or anything tied to this brand/stack. Default to project when there's no cross-project signal. - Ambiguous and clearly about personal taste → ask once, one line: "¿solo en este proyecto o en todos los tuyos?" Then write to the chosen store.
How to write
1. Create the dated atomic file with frontmatter; capture the Why, not just the what. 2. Add a one-line entry to that store's INDEX.md (id, hook, tags). 3. Phrase Apply as a rule actionable without the original context. 4. Confirm in one line where it landed: "Anotado en memoria de proyecto: nunca gradientes en hero aquí." Silent memory is spooky — keep the user in the loop.
Promotion & the upstream funnel
A correction can travel up three different paths — pick by how widely it's true:
| The correction is true for… | Destination |
|---|---|
| Only this codebase | Project store |
| All of the user's work | User/global store (promote with "do this everywhere": copy the file to ~/.ui-craft/memory/, mark the project copy superseded by global) |
| Most projects, for everyone | Upstream: it's not memory, it's a baseline gap — suggest a PR against the skill's references/* |
Test: would this be right on most projects, for most people? Yes → upstream candidate. Right for this one user everywhere → global. Right only here → project. Local memory is the discovery pipeline for improving the skill itself.
Hygiene
Memory rots like any notes store. Keep both healthy (run /ui-craft:memory-lint):
- One memory per file; date every file (ISO); newest hooks at the top of INDEX.
- A new memory that contradicts an old one marks the old
status: supersededand lists it insupersedes— don't leave two live entries that conflict. - Superseded files stay (so the history of why survives) but are never applied; the lint can archive them.
- If two active memories conflict and neither is superseded, ask the user which holds, then mark the loser.
- Cap each store. If a store's active
Learnedset grows past ~20, the most stable entries are real conventions — fold them intoprofile.md(project) or propose promoting them.
Optional engram bridge
The file store is canonical and works with zero dependencies. If the user's harness exposes an engram (or compatible) memory MCP, the skill may additionally mirror writes there (mem_save with a ui-craft/<project> topic key) and consult it at Discovery — gaining cross-session semantic recall on top. The bridge is strictly additive: never required, and the files remain the source of truth so the skill behaves identically for users without it.
Related skills
FAQ
What does ui-craft help developers build?
ui-craft helps developers craft user interfaces for SaaS products, agent tools, and extensions. The skill guides layout, component choices, and interaction patterns during frontend work.
When should ui-craft trigger in a project?
ui-craft triggers when developers need UI structure or visual polish while implementing frontend screens, especially for agent-facing or dashboard interfaces without finalized design specs.