
Design Lab
- 1.2k installs
- 743 repo stars
- Updated May 6, 2026
- 0xdesign/design-plugin
design-lab is a design-plugin agent skill that supplies battle-tested UX heuristics and interaction principles from established design systems for developers who want agents to generate or critique interfaces against pro
About
design-lab is a reference skill from the 0xdesign design-plugin that curates UX foundations and interaction principles from world-class designers and design systems for agent-driven UI work. Its Design Principles Reference document includes Jakob Nielsen's 10 Usability Heuristics—covering system status visibility, real-world matching, user control, consistency, and error prevention—alongside additional curated best practices for generating design variations. Developers reach for design-lab when prompting agents to produce interface mocks, critique flows, or align components with recognized usability patterns instead of improvised styling. The skill acts as a principles layer agents consult before outputting HTML, React, or design tokens.
- Includes Jakob Nielsen's complete 10 Usability Heuristics with direct application guidance
- Curated Don Norman principles covering affordances, signifiers, mapping, feedback and conceptual models
- Ready-to-use reference for generating design variations and interface critiques
- Helps eliminate common usability problems before user testing
Design Lab by the numbers
- 1,216 all-time installs (skills.sh)
- +49 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #341 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/0xdesign/design-plugin --skill design-labAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.2k |
|---|---|
| repo stars | ★ 743 |
| Security audit | 3 / 3 scanners passed |
| Last updated | May 6, 2026 |
| Repository | 0xdesign/design-plugin ↗ |
How do you apply Nielsen heuristics in agent UI work?
Reference battle-tested UX heuristics and interaction principles while prompting agents to generate or critique interfaces.
Who is it for?
Frontend developers using AI agents to generate or review interfaces who want Nielsen heuristics and design-system principles baked into prompts.
Skip if: Teams needing brand identity systems, pixel-perfect Figma handoff specs, or backend API design without interface usability concerns.
When should I use this skill?
An agent is generating UI variations, critiquing usability, or needs authoritative UX heuristics before proposing interface changes.
What you get
Heuristic-aligned UI critiques, interaction-principle checklists, and design variation briefs grounded in documented UX foundations.
- UX heuristic checklist
- Interface critique notes
- Design variation brief
By the numbers
- Documents Jakob Nielsen's 10 Usability Heuristics in its principles reference
Files
Design Lab Skill
This skill implements a complete design exploration workflow: interview, generate variations, collect feedback, refine, preview, and finalize.
CRITICAL: Cleanup Behavior
All temporary files MUST be deleted when the process ends, whether by:
- User confirms final design → cleanup, then generate plan
- User aborts/cancels → cleanup immediately, no plan generated
Never leave `.claude-design/` or `__design_lab` routes behind. If the user says "cancel", "abort", "stop", or "nevermind" at any point, confirm and then delete all temporary artifacts.
---
Phase 0: Preflight Detection
Before starting the interview, automatically detect:
Package Manager
Check for lock files in the project root:
pnpm-lock.yaml→ usepnpmyarn.lock→ useyarnpackage-lock.json→ usenpmbun.lockb→ usebun
Framework Detection
Check for config files:
next.config.jsornext.config.mjsornext.config.ts→ Next.js- Check for
app/directory → App Router - Check for
pages/directory → Pages Router vite.config.jsorvite.config.ts→ Viteremix.config.js→ Remixnuxt.config.jsornuxt.config.ts→ Nuxtastro.config.mjs→ Astro
Styling System Detection
Check package.json dependencies and config files:
tailwind.config.jsortailwind.config.ts→ Tailwind CSS@mui/materialin dependencies → Material UI@chakra-ui/reactin dependencies → Chakra UIantdin dependencies → Ant Designstyled-componentsin dependencies → styled-components@emotion/reactin dependencies → Emotion.cssor.module.cssfiles → CSS Modules
Design Memory Check
Look for existing Design Memory file:
docs/design-memory.mdDESIGN_MEMORY.md.claude-design/design-memory.md
If found, read it and use to prefill defaults and skip redundant questions.
Visual Style Inference (CRITICAL)
DO NOT use generic/predefined styles. Extract visual language from the project:
If Tailwind detected, read tailwind.config.js or tailwind.config.ts:
// Extract and use:
theme.colors // Color palette
theme.spacing // Spacing scale
theme.borderRadius // Radius values
theme.fontFamily // Typography
theme.boxShadow // Elevation systemIf CSS Variables exist, read globals.css, variables.css, or :root definitions:
:root {
--color-* /* Color tokens */
--spacing-* /* Spacing tokens */
--font-* /* Typography tokens */
--radius-* /* Border radius tokens */
}If UI library detected (MUI, Chakra, Ant), read the theme configuration:
- MUI:
theme.tsorcreateTheme()call - Chakra:
theme/index.tsorextendTheme()call - Ant:
ConfigProvidertheme prop
Always scan existing components to understand patterns:
- Find 2-3 existing buttons → note their styling patterns
- Find 2-3 existing cards → note padding, borders, shadows
- Find existing forms → note input styles, label placement
- Find existing typography → note heading sizes, body text
Store inferred styles in the Design Brief for consistent use across all variants.
---
Phase 1: Interview
Use the AskUserQuestion tool for all interview steps. Adapt questions based on Design Memory if it exists.
Step 1.1: Scope & Target
Ask these questions (can combine into single AskUserQuestion with multiple questions):
Question 1: Scope
- Header: "Scope"
- Question: "Are we designing a single component or a full page?"
- Options:
- "Component" - A reusable UI element (button, card, form, modal, etc.)
- "Page" - A complete page or screen layout
Question 2: New or Redesign
- Header: "Type"
- Question: "Is this a new design or a redesign of something existing?"
- Options:
- "New" - Creating something from scratch
- "Redesign" - Improving an existing component/page
If "Redesign" selected, ask: Question 3: Existing Path
- Header: "Location"
- Question: "What is the file path or route of the existing UI?"
- Options: (let user provide via "Other")
If target is unclear, propose a name based on repo patterns and confirm.
Step 1.2: Pain Points & Inspiration
Question 1: Pain Points
- Header: "Problems"
- Question: "What are the top pain points with the current design (or what should this new design avoid)?"
- Options:
- "Too cluttered/dense" - Information overload, hard to scan
- "Unclear hierarchy" - Primary actions aren't obvious
- "Poor mobile experience" - Doesn't work well on small screens
- "Outdated look" - Feels old or inconsistent with brand
- multiSelect: true
Question 2: Visual Inspiration
- Header: "Visual style"
- Question: "What products or brands should I reference for visual inspiration?"
- Options:
- "Stripe" - Clean, minimal, trustworthy
- "Linear" - Dense, keyboard-first, developer-focused
- "Notion" - Flexible, content-focused, playful
- "Apple" - Premium, spacious, refined
- multiSelect: true
Question 3: Functional Inspiration
- Header: "Interactions"
- Question: "What interaction patterns should I emulate?"
- Options:
- "Inline editing" - Edit in place without modals
- "Progressive disclosure" - Show more as needed
- "Optimistic updates" - Instant feedback, sync in background
- "Keyboard shortcuts" - Power user efficiency
Step 1.3: Brand & Style Direction
Question 1: Brand Adjectives
- Header: "Brand tone"
- Question: "What 3-5 adjectives describe the desired brand feel?"
- Options:
- "Minimal" - Clean, simple, uncluttered
- "Premium" - High-end, polished, refined
- "Playful" - Fun, friendly, approachable
- "Utilitarian" - Functional, efficient, no-nonsense
- multiSelect: true
Question 2: Density
- Header: "Density"
- Question: "What information density do you prefer?"
- Options:
- "Compact" - More information visible, tighter spacing
- "Comfortable" - Balanced spacing, easy scanning
- "Spacious" - Generous whitespace, focused attention
Question 3: Dark Mode
- Header: "Dark mode"
- Question: "Is dark mode required?"
- Options:
- "Yes" - Must support dark mode
- "No" - Light mode only
- "Nice to have" - Support if easy, not required
Step 1.4: Persona & Jobs-to-be-Done
Question 1: Primary User
- Header: "User"
- Question: "Who is the primary end user?"
- Options:
- "Developer" - Technical, keyboard-oriented
- "Designer" - Visual, detail-oriented
- "Business user" - Efficiency-focused, less technical
- "End consumer" - General public, varied technical ability
Question 2: Context
- Header: "Context"
- Question: "What's the primary usage context?"
- Options:
- "Desktop-first" - Primarily used on larger screens
- "Mobile-first" - Primarily used on phones
- "Both equally" - Must work well on all devices
Question 3: Key Tasks
- Header: "Key tasks"
- Question: "What are the top 3 tasks users must complete?"
- (Let user provide via "Other" - this is open-ended)
Step 1.5: Constraints
Question 1: Must-Keep Elements
- Header: "Keep"
- Question: "Are there elements that must be preserved?"
- Options:
- "Existing copy/labels" - Keep current text
- "Current fields/inputs" - Keep form structure
- "Navigation structure" - Keep current nav
- "None" - Free to change everything
Question 2: Technical Constraints
- Header: "Constraints"
- Question: "Any technical constraints?"
- Options:
- "No new dependencies" - Use existing libraries only
- "Use existing components" - Build on current design system
- "Must be accessible (WCAG)" - Strict accessibility requirements
- "None" - No special constraints
- multiSelect: true
---
Phase 2: Generate Design Brief
After the interview, create a structured Design Brief as JSON and save to .claude-design/design-brief.json:
{
"scope": "component|page",
"isRedesign": true|false,
"targetPath": "src/components/Example.tsx",
"targetName": "Example",
"painPoints": ["Too dense", "Primary action unclear"],
"inspiration": {
"visual": ["Stripe", "Linear"],
"functional": ["Inline validation"]
},
"brand": {
"adjectives": ["minimal", "trustworthy"],
"density": "comfortable",
"darkMode": true
},
"persona": {
"primary": "Developer",
"context": "desktop-first",
"keyTasks": ["Complete checkout", "Review order", "Apply discount"]
},
"constraints": {
"mustKeep": ["existing fields"],
"technical": ["no new dependencies", "WCAG accessible"]
},
"framework": "nextjs-app",
"packageManager": "pnpm",
"stylingSystem": "tailwind"
}Display a summary to the user before proceeding.
---
Phase 3: Generate Design Lab
Directory Structure
Create all files under .claude-design/:
.claude-design/
├── lab/
│ ├── page.tsx # Main lab page (framework-specific)
│ ├── variants/
│ │ ├── VariantA.tsx
│ │ ├── VariantB.tsx
│ │ ├── VariantC.tsx
│ │ ├── VariantD.tsx
│ │ └── VariantE.tsx
│ ├── components/
│ │ └── LabShell.tsx # Lab layout wrapper
│ ├── feedback/ # Interactive feedback system
│ │ ├── types.ts # TypeScript interfaces
│ │ ├── selector-utils.ts # Element identification
│ │ ├── format-utils.ts # Feedback formatting
│ │ ├── FeedbackOverlay.tsx # Main overlay component
│ │ └── index.ts # Module exports
│ └── data/
│ └── fixtures.ts # Shared mock data
├── design-brief.json
└── run-log.mdFeedback System Setup (CRITICAL - NEVER SKIP)
The FeedbackOverlay is the PRIMARY feature of the Design Lab. Without it, users cannot provide interactive feedback. NEVER generate a Design Lab without the FeedbackOverlay.
Reliability Strategy: To avoid import path issues across different project configurations, create the FeedbackOverlay directly in the route directory (e.g., app/design-lab/FeedbackOverlay.tsx), NOT in .claude-design/. This ensures a simple relative import (./FeedbackOverlay) always works.
Required Files in Route Directory:
app/design-lab/ # or app/__design_lab/ if underscores work
├── page.tsx # Main lab page with variants
└── FeedbackOverlay.tsx # Self-contained overlay component (copy from templates)Template Source: design-and-refine/templates/feedback/FeedbackOverlay.tsx
Why this approach:
.claude-design/paths can fail due to bundler configurations- Relative imports from the same directory always work
- The route directory gets deleted during cleanup anyway
Route Integration
Next.js App Router: Create app/__design_lab/page.tsx that imports from .claude-design/lab/
Next.js Pages Router: Create pages/__design_lab.tsx that imports from .claude-design/lab/
Vite React:
- If React Router exists: add route to
/__design_lab - If no router: create a conditional render in
App.tsxbased on?design_lab=truequery param
Other frameworks: Create the most appropriate temporary route for the detected framework.
Variant Generation Guidelines
IMPORTANT: Read DESIGN_PRINCIPLES.md for UX, interaction, and motion best practices. But DO NOT use predefined visual styles—infer them from the project.
Apply universal principles (from DESIGN_PRINCIPLES.md):
- UX: Nielsen's heuristics, cognitive load reduction, progressive disclosure
- Component behavior: Button states, form anatomy, card structure
- Interaction: Feedback patterns, state handling, optimistic updates
- Motion: Timing (150-300ms), easing (ease-out entrances, ease-in exits)
- Accessibility: Focus states, ARIA patterns, touch targets (44px min)
Infer visual styles from the project:
- Colors → from Tailwind config, CSS variables, or existing components
- Typography → from existing headings, body text in the codebase
- Spacing → from the project's spacing scale or existing patterns
- Border radius → from existing cards, buttons, inputs
- Shadows → from existing elevated components
---
Each variant MUST explore a different design axis. Do not create minor variations—make them meaningfully distinct. Use the project's existing visual language for all variants.
Variant A: Information Hierarchy Focus
- Restructure content hierarchy (what's most important?)
- Apply Gestalt proximity—group related items closer
- One primary action per view
- Use existing typography scale to create clear levels
Variant B: Layout Model Exploration
- Try a different layout approach (card vs list vs table vs split-pane)
- Apply card anatomy or table behavior patterns from DESIGN_PRINCIPLES
- Consider responsive behavior at each breakpoint
- Use the project's existing grid/layout system
Variant C: Density Variation
- If brief says "comfortable", try a more compact version
- If brief says "compact", try a more spacious version
- Use the project's existing spacing tokens—just apply them differently
- Show the tradeoffs: more visible data vs easier scanning
Variant D: Interaction Model
- Different interaction pattern (modal vs inline vs panel vs drawer)
- Apply feedback patterns: immediate → progress → completion
- Implement all required states (loading, error, empty, disabled)
- Consider optimistic updates for non-destructive actions
Variant E: Expressive Direction
- Push the brand direction the user described in the interview
- Explore different uses of the project's existing design tokens
- More or less use of shadows, borders, background colors
- Apply motion where it adds meaning (hover, focus, transitions)
Lab Page Requirements
The Design Lab page must include:
1. Header with:
- Design Brief summary (target, scope, key requirements)
- Instructions for reviewing
2. Variant Grid with:
- Clear labels (A, B, C, D, E)
- Brief rationale for each variant ("Why this exists")
- The actual rendered variant
- Notes highlighting key differences
- IMPORTANT: Each variant container must have
data-variant="X"attribute (where X is A, B, C, D, E, or F). This is required for the feedback system to identify which variant comments belong to.
3. Responsive behavior:
- Desktop: side-by-side grid (2-3 columns)
- Mobile: horizontal scroll or tabs
4. Shared Data:
- All variants use the same fixture data from
data/fixtures.ts - Ensures fair comparison
5. Feedback Overlay (CRITICAL - NEVER OMIT):
⚠️ THIS IS THE MOST IMPORTANT REQUIREMENT ⚠️
The FeedbackOverlay enables users to click on elements and leave comments. Without it, the Design Lab is just a static page with no way to collect structured feedback.
- Create
FeedbackOverlay.tsxin the SAME directory aspage.tsx - Import with relative path:
import { FeedbackOverlay } from './FeedbackOverlay' - Render at the END of the page, after all variants
- Pass
targetNameprop with the component/page name
Example integration:
import { FeedbackOverlay } from './FeedbackOverlay'; // Relative import - always works
export default function DesignLabPage() {
return (
<div className="min-h-screen bg-background">
<header>...</header>
<main>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<div data-variant="A">
<VariantA />
</div>
<div data-variant="B">
<VariantB />
</div>
{/* ... more variants */}
</div>
</main>
{/* CRITICAL: FeedbackOverlay must be included */}
<FeedbackOverlay targetName="ComponentName" />
</div>
);
}If you forget the FeedbackOverlay, the user CANNOT provide feedback. This defeats the entire purpose of the Design Lab.
Code Quality
Conventions:
- Follow the project's existing code conventions (file naming, imports, etc.)
- Use the detected styling system (Tailwind, CSS modules, etc.)
- Use existing components from the project where appropriate
Accessibility (from DESIGN_PRINCIPLES):
- Semantic HTML:
<button>not<div onclick>,<nav>,<main>,<section> - Keyboard navigation: all interactive elements focusable and operable
- Focus states: visible
:focus-visiblewith 2px ring and offset - Color contrast: 4.5:1 for text, 3:1 for UI elements
- Touch targets: minimum 44x44px
- ARIA only when HTML semantics aren't enough
States (every component needs):
- Default, Hover, Focus, Active, Disabled, Loading, Error, Empty
- See DESIGN_PRINCIPLES "State Handling" section
Motion:
- Use appropriate timing: 150-200ms for micro-interactions, 200-300ms for transitions
- Use ease-out for entrances, ease-in for exits
- Respect
prefers-reduced-motion
---
Phase 4: Present Design Lab to User
After generating the lab files, immediately present the lab to the user. Do NOT attempt to:
- Start the dev server yourself (it runs forever and will block)
- Check if ports are open
- Open a browser
- Wait for any server response
What to Do
1. Output the lab location and URL:
✅ Design Lab created!
I've generated 5 design variants in `.claude-design/lab/`
To view them:
1. Make sure your dev server is running (run `pnpm dev` if not)
2. Open: http://localhost:3000/__design_lab
Take your time reviewing the variants side-by-side, then come back and tell me:
- Which variant wins (A-E)
- What you like about it
- What should change2. Immediately proceed to Phase 5 - ask for feedback. Do NOT wait for the user to say they've opened the browser. Just present the feedback questions right away so they're ready when the user returns.
Why Not Start the Server
Running pnpm dev or npm run dev starts a long-running process that never exits. If you run it, you'll wait forever. The user likely already has their dev server running, or can start it themselves in another terminal.
---
Phase 5: Collect Feedback
After presenting the lab URL, the user can provide feedback in two ways: 1. Interactive Feedback (recommended): Using the built-in overlay in the browser 2. Manual Feedback: Via AskUserQuestion in the terminal
Interactive Feedback (Primary Method)
The Design Lab includes a Figma-like feedback overlay. When presenting the lab, include these instructions:
✅ Design Lab created!
I've generated 5 design variants in `.claude-design/lab/`
To view and provide feedback:
1. Make sure your dev server is running (run `pnpm dev` if not)
2. Open: http://localhost:3000/__design_lab
**To add feedback:**
1. Click the "Add Feedback" button (bottom-right corner)
2. Click any element you want to comment on
3. Type your feedback and click "Save"
4. Repeat for all elements you want to comment on
5. Fill in the "Overall Direction" field (required)
6. Click "Submit All Feedback"
7. Paste the copied text here in the terminal
Or just describe your feedback manually below!When the user pastes feedback, it will be in this format:
## Design Lab Feedback
**Target:** ComponentName
**Comments:** 3
### Variant A
1. **Button** (`[data-testid='submit']`, button with "Submit")
"Make this more prominent"
### Variant B
1. **Card** (`.product-card`, div with "Product Name")
"Love this layout"
### Overall Direction
Go with Variant B's structure. Apply Variant A's button styling.How to parse and act on this feedback:
1. Read the Overall Direction first - this guides your synthesis 2. For each comment, locate the element using:
- Primary: The CSS selector in backticks (e.g.,
[data-testid='submit']) - Secondary: The element description (e.g., "button with 'Submit'")
3. Apply the feedback by editing the corresponding variant file
Fallback: Manual Feedback via AskUserQuestion
If the user prefers not to use the interactive overlay (or pastes manual feedback), use the AskUserQuestion flow below:
Stage 1: Check for a Winner
Question 1: Ready to pick?
- Header: "Decision"
- Question: "Is there one variant you like as is?"
- Options:
- "Yes - I found one I like" - Ready to select a winner and refine
- "No - I like parts of different ones" - Need to synthesize a new variant
Stage 2A: If User Found a Winner
If user said "Yes", ask:
Question 2a: Which one?
- Header: "Winner"
- Question: "Which variant do you want to go with?"
- Options:
- "Variant A" - [brief description of A]
- "Variant B" - [brief description of B]
- "Variant C" - [brief description of C]
- "Variant D" - [brief description of D]
- "Variant E" - [brief description of E]
Question 3a: Any tweaks?
- Header: "Tweaks"
- Question: "Any small changes needed, or is it good as is?"
- Options:
- "Good as is" - No changes needed, proceed to final preview
- "Minor tweaks needed" - I'll describe what to adjust
If "Minor tweaks needed", ask user to describe changes via text input.
Then proceed to Phase 7: Final Preview.
Stage 2B: If User Wants to Synthesize
If user said "No - I like parts of different ones", ask:
Question 2b: What do you like about each?
- Header: "Feedback"
- Question: "What do you like about each variant? (mention specific elements from A, B, C, D, E)"
- (Let user provide detailed feedback via "Other" text input)
Example response format to guide user:
- A: Love the card layout and spacing
- B: The color scheme feels right
- C: The interaction on hover is great
- D: Nothing stands out
- E: The typography hierarchy is clearestThen proceed to Phase 6: Synthesize New Variant.
---
Phase 6: Synthesize New Variant
Based on the user's feedback about what they liked from each variant:
1. Create a new hybrid variant (Variant F) that combines:
- The specific elements the user called out from each
- The best structural decisions across all variants
- Any patterns that appeared in multiple variants
2. Replace the Design Lab with a comparison view:
- Show the new synthesized Variant F prominently
- Keep 1-2 of the original variants that were closest for comparison
- Remove variants that had nothing the user liked
3. Update the `/__design_lab` route to show the new arrangement
4. Ask for feedback again:
Question: How's the new variant?
- Header: "Review"
- Question: "How does the synthesized variant (F) look?"
- Options:
- "This is it!" - Proceed to final preview
- "Getting closer" - Need another iteration
- "Went the wrong direction" - Let me clarify what I want
If "Getting closer" or "Went the wrong direction", gather more specific feedback and iterate. Support multiple synthesis passes until user is satisfied.
Then proceed to Phase 7: Final Preview.
---
Phase 7: Final Preview
Once user is satisfied:
1. Create .claude-design/preview/ directory:
.claude-design/preview/
├── page.tsx # Preview page
└── FinalDesign.tsx # The winning design2. Create route at /__design_preview
3. For redesigns, include before/after comparison:
- Toggle switch or split view
- Show original alongside proposed
4. Ask for final confirmation:
Question: Confirm final design?
- Header: "Confirm"
- Question: "Ready to finalize this design?"
- Options:
- "Yes, finalize it" - Proceed to cleanup and generate implementation plan
- "No, needs changes" - Tell me what to adjust
- "Abort - cancel everything" - Delete all temp files, no plan generated
If "No, needs changes": gather feedback and iterate. If "Abort": proceed to Abort Handling below.
---
Abort Handling
If the user wants to cancel/abort at ANY point during the process (not just final confirmation), they may say things like:
- "cancel"
- "abort"
- "stop"
- "nevermind"
- "forget it"
- "I changed my mind"
When abort is detected:
1. Confirm the abort:
- "Are you sure you want to cancel? This will delete all the design lab files I created."
2. If confirmed, clean up immediately:
- Delete
.claude-design/directory entirely - Delete temporary route files (
app/__design_lab/, etc.) - Do NOT generate any implementation plan
- Do NOT update Design Memory
3. Acknowledge:
- "Design exploration cancelled. All temporary files have been cleaned up. Let me know if you want to start fresh later."
---
Phase 8: Finalize
When user confirms (selected "Yes, finalize it"):
8.1: Cleanup
Delete all temporary files:
- Remove
.claude-design/directory entirely - Remove temporary route files:
app/__design_lab/(Next.js App Router)pages/__design_lab.tsx(Next.js Pages Router)app/__design_preview/pages/__design_preview.tsx- Revert any
App.tsxmodifications (Vite)
Safety rules:
- ONLY delete files inside
.claude-design/ - ONLY delete route files that the plugin created
- NEVER delete user-authored files
- Verify file paths before deletion
8.2: Generate Implementation Plan
Create DESIGN_PLAN.md in the project root:
# Design Implementation Plan: [TargetName]
## Summary
- **Scope:** [component/page]
- **Target:** [file path]
- **Winner variant:** [A-E]
- **Key improvements:** [from feedback]
## Files to Change
- [ ] `src/components/Example.tsx` - Main component refactor
- [ ] `src/styles/example.css` - Style updates
- [ ] ... (list all affected files)
## Implementation Steps
1. [Specific step with code guidance]
2. [Next step]
3. ...
## Component API
- **Props:**
- `prop1: type` - description
- ...
- **State:**
- Internal state requirements
- **Events:**
- Callbacks and handlers
## Required UI States
- **Loading:** [description]
- **Empty:** [description]
- **Error:** [description]
- **Disabled:** [description]
- **Validation:** [description]
## Accessibility Checklist
- [ ] Keyboard navigation works
- [ ] Focus states visible
- [ ] Labels and aria-* attributes correct
- [ ] Color contrast meets WCAG AA
- [ ] Screen reader tested
## Testing Checklist
- [ ] Unit tests for logic
- [ ] Component tests for rendering
- [ ] Visual regression tests (if applicable)
- [ ] E2E smoke test (if applicable)
## Design Tokens
- [Any new tokens to add]
- [Existing tokens to use]
---
*Generated by Design Variations plugin*8.3: Update Design Memory
Create or update DESIGN_MEMORY.md:
If new file:
# Design Memory
## Brand Tone
- **Adjectives:** [from interview]
- **Avoid:** [anti-patterns discovered]
## Layout & Spacing
- **Density:** [preference]
- **Grid:** [if established]
- **Corner radius:** [if consistent]
- **Shadows:** [if consistent]
## Typography
- **Headings:** [font, weights used]
- **Body:** [font, size]
- **Emphasis:** [patterns]
## Color
- **Primary:** [color tokens]
- **Secondary:** [color tokens]
- **Neutral strategy:** [approach]
- **Semantic colors:** [error, success, warning]
## Interaction Patterns
- **Forms:** [validation approach, layout]
- **Modals/Drawers:** [when to use which]
- **Tables/Lists:** [preferred patterns]
- **Feedback:** [toast, inline, etc.]
## Accessibility Rules
- **Focus:** [visible focus approach]
- **Labels:** [labeling conventions]
- **Motion:** [reduced motion support]
## Repo Conventions
- **Component structure:** [file organization]
- **Styling approach:** [Tailwind classes, CSS modules, etc.]
- **Existing primitives:** [Button, Input, Card, etc.]
---
*Updated by Design Variations plugin*If updating existing file:
- Append new patterns discovered
- Update any conflicting guidance with latest decisions
- Keep file concise and actionable
---
Error Handling
Framework Not Detected
If framework cannot be determined:
- Ask user: "I couldn't detect your framework. What are you using?"
- Provide common options: Next.js, Vite, Create React App, Vue, etc.
Dev Server Fails
If dev server won't start:
- Check for port conflicts
- Provide manual instructions
- Suggest user starts server themselves
Route Integration Fails
If can't create temporary route:
- Fall back to creating standalone HTML file
- Provide instructions for manual preview
Cleanup Interrupted
If cleanup is interrupted:
- Log what was deleted vs remaining
- Provide manual cleanup instructions
- Never leave partial state without informing user
---
Configuration Options
The plugin supports these optional configurations (via environment or project config):
DESIGN_AUTO_IMPLEMENT: Iftrue, implement the plan immediately after confirmationDESIGN_KEEP_LAB: Iftrue, don't delete lab until explicit cleanup commandDESIGN_MEMORY_PATH: Custom path for Design Memory file
---
Example Session Flow
1. User: /design-variations:design CheckoutSummary 2. Plugin detects: Next.js App Router, Tailwind, pnpm 3. Plugin finds: No existing Design Memory 4. Plugin asks: Interview questions (5 steps) 5. Plugin generates: Design Brief summary 6. Plugin creates: .claude-design/lab/ with 5 variants 7. Plugin creates: app/__design_lab/page.tsx 8. Plugin starts: pnpm dev 9. Plugin outputs: "Open http://localhost:3000/__design_lab" 10. User reviews variants in browser 11. Plugin asks: "Which variant wins?" 12. User: "Variant C, but change X and Y" 13. Plugin refines: Updates Variant C 14. User: "Looks good" 15. Plugin creates: Final preview at /__design_preview 16. User: "Confirmed" 17. Plugin: Deletes all temp files 18. Plugin: Generates DESIGN_PLAN.md 19. Plugin: Creates DESIGN_MEMORY.md 20. Plugin: "Done! See DESIGN_PLAN.md for implementation steps"
Design Principles Reference
This document contains curated best practices from world-class designers and design systems. Reference these principles when generating design variations.
---
Part 1: UX Foundations
Jakob Nielsen's 10 Usability Heuristics
1. Visibility of system status - Always keep users informed through appropriate feedback within reasonable time 2. Match between system and real world - Use familiar language, concepts, and conventions 3. User control and freedom - Provide clear "emergency exits" (undo, cancel, back) 4. Consistency and standards - Follow platform conventions; same words mean same things 5. Error prevention - Eliminate error-prone conditions or ask for confirmation 6. Recognition over recall - Minimize memory load; make options visible 7. Flexibility and efficiency - Provide accelerators for expert users (shortcuts, defaults) 8. Aesthetic and minimalist design - Remove irrelevant information; every element competes 9. Help users recover from errors - Plain language errors with constructive solutions 10. Help and documentation - Provide concise, task-focused help when needed
Don Norman's Design Principles
- Affordances - Design elements should suggest their usage
- Signifiers - Visual cues that indicate where actions should happen
- Mapping - Controls should relate spatially to their effects
- Feedback - Every action needs a perceivable response
- Conceptual model - Users should understand how the system works
Cognitive Load Principles
- Limit choices - 5-7 items max in navigation; 3-4 options in decisions
- Progressive disclosure - Show only what's needed at each step
- Chunking - Group related items; break long forms into steps
- Visual hierarchy - Guide attention with size, color, contrast, position
- Reduce cognitive friction - Minimize decisions, clicks, and reading
URL & State Principles
- URL state reflection - Important UI state (filters, tabs, pagination) should be in the URL
- Shareable links - Users should be able to share/bookmark the current view
- Browser navigation - Back/forward buttons should work as expected
Destructive Actions
- Confirmation required - Delete, remove, and irreversible actions need explicit confirmation
- Clear consequences - State exactly what will happen ("This will permanently delete 5 files")
- Recovery path - Prefer soft delete with undo over immediate permanent deletion
- Visual distinction - Destructive buttons use warning colors (red) and distinct styling
---
Part 2: Visual Design Systems
Typography (from iA, Stripe, Linear)
Hierarchy:
Display: 32-48px, -0.02em tracking, 700 weight
Heading 1: 24-32px, -0.02em tracking, 600 weight
Heading 2: 20-24px, -0.01em tracking, 600 weight
Heading 3: 16-18px, normal tracking, 600 weight
Body: 14-16px, normal tracking, 400 weight
Caption: 12-13px, +0.01em tracking, 400-500 weightBest practices:
- Max 60-75 characters per line for readability
- Line height: 1.4-1.6 for body text, 1.2-1.3 for headings
- Use weight contrast (400 vs 600) more than size contrast
- Limit to 2 font families maximum
- System fonts for performance:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif
Typographic details:
- Use proper ellipsis
…not...(three dots) - Use curly quotes
""not straight quotes" - Non-breaking spaces for values:
10 MB,5 items(use or\u00A0) font-variant-numeric: tabular-numsfor numbers in tables, counters, pricestext-wrap: balancefor headings (prevents orphans/widows)text-wrap: prettyfor body text (better line breaks)
Spacing System (8px grid)
4px - Tight: icon padding, inline spacing
8px - Base: related elements, form field padding
12px - Comfortable: between form fields
16px - Standard: section padding, card padding
24px - Relaxed: between sections
32px - Spacious: major section breaks
48px - Generous: page section separation
64px+ - Hero: landing page sectionsSpacing principles:
- Related items closer together (Gestalt proximity)
- Consistent internal padding (all sides equal, or vertical > horizontal)
- White space is not wasted space—it creates focus
- Touch targets minimum 44x44px (Apple HIG)
Color (from Stripe, Linear, Vercel)
Neutral foundation:
Background: #FFFFFF / #000000 (dark)
Surface: #FAFAFA / #111111 (dark)
Border: #E5E5E5 / #333333 (dark)
Text primary: #171717 / #EDEDED (dark)
Text secondary: #737373 / #A3A3A3 (dark)
Text tertiary: #A3A3A3 / #737373 (dark)Accent usage:
- Primary action: single brand color, used sparingly
- Interactive elements: consistent color for all clickable items
- Semantic colors: red (error), green (success), yellow (warning), blue (info)
- Hover states: 10% darker or add subtle background
- Focus states: 2px ring with offset, high contrast
Color principles:
- WCAG AA minimum: 4.5:1 for text, 3:1 for UI elements
- One primary accent color; avoid rainbow interfaces
- Use opacity for secondary states (hover, disabled)
- Dark mode: don't just invert—reduce contrast, use darker surfaces
Dark mode setup:
<!-- On <html> element -->
<html class="dark" style="color-scheme: dark">
<!-- Theme color matching page background -->
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">Content Handling
Text truncation:
/* Single line truncation */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Multi-line truncation */
.line-clamp-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
/* Break long words */
.break-words {
overflow-wrap: break-word;
word-break: break-word;
}Flex children with text:
/* IMPORTANT: Flex children with text need min-w-0 to truncate properly */
.flex-child-with-text {
min-width: 0; /* Allows text to shrink below content size */
}Empty states:
- Always design the empty state—it's the first thing users see
- Include helpful message + primary action
- Use illustration or icon to add visual interest
Images:
// Always include explicit dimensions to prevent layout shift
<img
src="/image.jpg"
width={800}
height={600}
alt="Description"
loading="lazy" // Defer off-screen images
/>
// For above-the-fold images
<img src="/hero.jpg" width={1200} height={800} alt="Hero" priority />Border Radius (from modern SaaS)
None (0px): Tables, dividers, full-bleed images
Small (4px): Buttons, inputs, tags, badges
Medium (8px): Cards, modals, dropdowns
Large (12px): Feature cards, hero elements
Full (9999px): Avatars, pills, toggle tracksPrinciples:
- Consistency: pick 2-3 radius values and stick to them
- Nested elements: inner radius = outer radius - padding
- Sharp corners feel technical/precise; round feels friendly/approachable
Shadows & Elevation (from Material, Linear)
Level 0: none (flat, on surface)
Level 1: 0 1px 2px rgba(0,0,0,0.05) - Subtle lift (cards)
Level 2: 0 4px 6px rgba(0,0,0,0.07) - Raised (dropdowns)
Level 3: 0 10px 15px rgba(0,0,0,0.1) - Floating (modals)
Level 4: 0 20px 25px rgba(0,0,0,0.15) - High (popovers)Principles:
- Shadows should feel like natural light (top-down, slight offset)
- Dark mode: use lighter surface colors instead of shadows
- Combine with subtle border for definition
- Interactive elements can elevate on hover
---
Part 3: Component Patterns
Buttons (from Stripe, Linear)
Hierarchy:
1. Primary - One per view, main action, filled with brand color 2. Secondary - Supporting actions, outlined or ghost style 3. Tertiary - Low-emphasis actions, text-only with hover state 4. Destructive - Delete/remove actions, red with confirmation
States:
- Default → Hover (+shadow or darken) → Active (scale 0.97) → Disabled (50% opacity)
- Loading: replace text with spinner, maintain width
- Min width: 80px; min height: 36px (touch-friendly: 44px)
Best practices:
- Specific labels: "Save API Key" not "Continue" or "Submit"
- Verb + noun labels: "Create project" not "Create"
- Sentence case, not ALL CAPS
- Icon left of text (or icon-only with tooltip)
- Primary button right-aligned in forms/dialogs
- Icon buttons require `aria-label`
Active state feedback:
button:active {
transform: scale(0.97);
}Forms (from Airbnb, Stripe, Vercel)
Input anatomy:
┌─────────────────────────────────┐
│ Label │ ← Required (above input, not inside)
│ ┌─────────────────────────────┐ │
│ │ Placeholder... │ │ ← Format hint only, ends with ...
│ └─────────────────────────────┘ │
│ Helper text or error message │ ← Specific and actionable
└─────────────────────────────────┘Autocomplete attributes (required):
<!-- Always use appropriate autocomplete for user data -->
<input type="email" autocomplete="email" />
<input type="text" autocomplete="name" />
<input type="text" autocomplete="given-name" />
<input type="text" autocomplete="family-name" />
<input type="text" autocomplete="organization" />
<input type="text" autocomplete="street-address" />
<input type="text" autocomplete="postal-code" />
<input type="tel" autocomplete="tel" />
<input type="password" autocomplete="current-password" />
<input type="password" autocomplete="new-password" />
<input type="text" autocomplete="one-time-code" />Input types and modes:
<!-- Use correct type for validation and keyboard -->
<input type="email" inputmode="email" />
<input type="tel" inputmode="tel" />
<input type="url" inputmode="url" />
<input type="number" inputmode="numeric" />
<!-- Numeric input without spinners -->
<input type="text" inputmode="numeric" pattern="[0-9]*" />Disable spellcheck where inappropriate:
// Disable for codes, emails, usernames, URLs
<input type="text" spellCheck={false} autoComplete="username" />
<input type="email" spellCheck={false} />
<input type="text" spellCheck={false} placeholder="Enter code..." />Anti-patterns to avoid:
// NEVER block paste - this is hostile UX
<input onPaste={(e) => e.preventDefault()} /> // ❌ NEVER DO THIS
// NEVER use placeholder as label
<input placeholder="Email" /> // ❌ Placeholder disappears on focus
// NEVER validate on every keystroke
onChange={(e) => validateEmail(e.target.value)} // ❌ Too aggressiveBest practices:
- Labels above inputs (not inside—accessibility)
- Placeholder ≠ label; use for format hints only, end with
... - Inline validation on blur, not on every keystroke
- Error messages: specific and actionable ("Email must include @")
- Focus first error field after form submission fails
- Success state: checkmark icon, green border (brief)
- Required fields: mark optional ones instead of required
- Single column forms outperform multi-column
Unsaved changes warning:
// Warn users before leaving with unsaved changes
useEffect(() => {
const handleBeforeUnload = (e) => {
if (hasUnsavedChanges) {
e.preventDefault();
e.returnValue = '';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [hasUnsavedChanges]);Cards (from Material, Apple)
Anatomy:
┌────────────────────────────────┐
│ [Media/Image] │ ← Optional
├────────────────────────────────┤
│ Eyebrow · Metadata │ ← Optional
│ Title │ ← Required
│ Description text that can │ ← Optional
│ wrap to multiple lines... │
├────────────────────────────────┤
│ [Actions] [More] │ ← Optional
└────────────────────────────────┘Best practices:
- Entire card clickable for primary action
- Consistent padding (16-24px)
- Image aspect ratios: 16:9, 4:3, 1:1 (be consistent)
- Limit to 2 actions max; overflow to menu
- Hover: subtle lift (translateY -2px + shadow increase)
Tables (from Linear, Notion)
Best practices:
- Left-align text, right-align numbers
- Use `tabular-nums` for numeric columns (consistent width digits)
- Zebra striping OR row hover, not both
- Sticky header on scroll
- Sortable columns: show current sort indicator
- Actions: row hover reveals action buttons (or kebab menu)
- Empty state: helpful message + action
- Pagination vs infinite scroll: pagination for data accuracy, infinite for browsing
- Min row height: 48px for touch; 40px for dense
- Virtualize tables with >50 rows
.numeric-column {
font-variant-numeric: tabular-nums;
text-align: right;
}Navigation (from Apple HIG, Material)
Patterns by scale:
- 2-5 items: Tab bar / horizontal tabs
- 5-10 items: Side navigation (collapsible)
- 10+ items: Side nav with sections/groups
Best practices:
- Current location always visible
- Breadcrumbs for deep hierarchy (not for flat structures)
- Mobile: bottom nav for primary actions (thumb-friendly)
- Icons + labels together; icon-only needs tooltip
- Consistent order across pages
---
Part 4: Interaction Design
Feedback Patterns (from Dan Saffer's Microinteractions)
Every action needs feedback:
1. Immediate - Button press visual (scale, color change) 2. Progress - Loading states for anything >1s 3. Completion - Success confirmation (toast, checkmark, animation) 4. Failure - Clear error with recovery path
Loading states:
- 0-100ms: No indicator needed
- 100-300ms: Subtle change (opacity, skeleton)
- 300ms-1s: Spinner or progress bar
- 1s+: Skeleton screens + progress indication
- 10s+: Background processing with notification
State Handling
Every component needs these states:
Default → Base appearance
Hover → Interactive hint (cursor change, highlight)
Focus → Keyboard navigation (visible ring)
Active → Being pressed/activated
Loading → Async operation in progress
Disabled → Not available (reduce opacity, remove pointer)
Error → Invalid input or failed operation
Success → Completed successfully (brief)
Empty → No data to display (helpful message + action)Touch & Pointer Interactions
Faster tap response:
/* Remove 300ms tap delay on touch devices */
button, a, [role="button"] {
touch-action: manipulation;
}Contain scroll in modals:
/* Prevent scroll chaining to body when modal/drawer reaches edge */
.modal, .drawer, .dropdown {
overscroll-behavior: contain;
}Touch targets:
- Minimum 44x44px for all interactive elements (Apple HIG)
- Provide adequate spacing between targets (8px minimum)
Hover states for pointer devices only:
/* Only apply hover effects on devices with fine pointers */
@media (hover: hover) and (pointer: fine) {
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
}Tap highlight:
/* Customize or remove tap highlight on mobile */
button {
-webkit-tap-highlight-color: transparent; /* Remove default */
/* Or use a custom color */
-webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);
}Optimistic Updates (from Linear, Notion)
- Update UI immediately, sync in background
- Show subtle "Saving..." indicator
- On failure: revert UI + show error toast with retry
- Best for: toggles, reordering, text edits
- Avoid for: destructive actions, payments
Progressive Disclosure
Reveal complexity gradually:
- Show essential options first
- "Advanced" or "More options" for power features
- Inline expansion over page navigation
- Tooltips for supplementary information
- Context menus for secondary actions
Inferring Intent
Anticipate user actions before they happen:
// Preload on mousedown (fires before click)
<button
onMouseDown={() => prefetchData()}
onClick={() => showData()}
>
View Details
</button>
// Preload on hover for links
<Link
href="/dashboard"
onMouseEnter={() => router.prefetch('/dashboard')}
>
Dashboard
</Link>Proximity-based preloading:
// Start loading when cursor approaches
function useProximityPreload(ref, onApproach) {
useEffect(() => {
const element = ref.current;
const handleMouseMove = (e) => {
const rect = element.getBoundingClientRect();
const distance = Math.hypot(
e.clientX - (rect.left + rect.width / 2),
e.clientY - (rect.top + rect.height / 2)
);
if (distance < 100) onApproach();
};
document.addEventListener('mousemove', handleMouseMove);
return () => document.removeEventListener('mousemove', handleMouseMove);
}, [ref, onApproach]);
}Smart defaults:
- Pre-fill forms with likely values
- Remember user's last selection
- Use geolocation for location fields
- Default date pickers to sensible dates (today, tomorrow)
Interaction Metaphors
Physical analogies users already understand:
| Gesture | Real-world Metaphor | UI Behavior |
|---|---|---|
| Drag | Moving physical objects | Reorder, move items |
| Swipe | Flipping pages, pushing aside | Navigate, dismiss |
| Pinch | Zooming a camera lens | Scale content |
| Pull down | Stretching a spring | Refresh content |
| Long press | Pressing firmly to reveal | Context menu |
Consistency requirement:
Once you establish a gesture metaphor, use it consistently:
❌ Swipe right to delete in one view, swipe right to archive in another
✅ Swipe right always archives, swipe left always deletesHonor platform conventions:
- iOS: Swipe from left edge = back navigation
- Android: Back button/gesture = return to previous screen
- Desktop: Right-click = context menu
Ergonomic Interactions
Expand hit areas with pseudo-elements:
/* Thin visual element with large tap target */
.icon-button {
position: relative;
width: 24px;
height: 24px;
}
.icon-button::after {
content: '';
position: absolute;
inset: -12px; /* Expands hit area to 48x48px */
}Bidirectional scroll support:
/* Support both LTR and RTL scrolling */
.horizontal-scroll {
overflow-x: auto;
scroll-behavior: smooth;
/* Use logical properties */
scroll-padding-inline: 16px;
}Thumb-friendly mobile zones:
┌─────────────────────────────────┐
│ Hard to reach (top) │ ← Avoid primary actions here
├─────────────────────────────────┤
│ │
│ Comfortable middle │ ← Secondary actions OK
│ │
├─────────────────────────────────┤
│ Easy reach (bottom) │ ← Primary actions here
└─────────────────────────────────┘Contained Gestures
Prevent gesture conflicts with parent elements:
/* Contain drag/swipe gestures within element */
.draggable-area {
touch-action: none; /* Disable browser handling */
user-select: none; /* Prevent text selection during drag */
}
/* Allow vertical scroll but capture horizontal */
.horizontal-swipe {
touch-action: pan-y; /* Allow vertical, capture horizontal */
}Pointer capture for drag operations:
function useDrag(onDrag, onDragEnd) {
const handlePointerDown = (e) => {
e.currentTarget.setPointerCapture(e.pointerId);
};
const handlePointerMove = (e) => {
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
onDrag({ x: e.clientX, y: e.clientY });
}
};
const handlePointerUp = (e) => {
e.currentTarget.releasePointerCapture(e.pointerId);
onDragEnd();
};
return {
onPointerDown: handlePointerDown,
onPointerMove: handlePointerMove,
onPointerUp: handlePointerUp,
};
}Drag threshold detection:
// Distinguish click from drag with movement threshold
const DRAG_THRESHOLD = 5; // pixels
function useDragThreshold() {
const startPos = useRef(null);
const [isDragging, setIsDragging] = useState(false);
const handlePointerDown = (e) => {
startPos.current = { x: e.clientX, y: e.clientY };
};
const handlePointerMove = (e) => {
if (!startPos.current) return;
const distance = Math.hypot(
e.clientX - startPos.current.x,
e.clientY - startPos.current.y
);
if (distance > DRAG_THRESHOLD) {
setIsDragging(true);
}
};
const handlePointerUp = (e) => {
const wasDragging = isDragging;
setIsDragging(false);
startPos.current = null;
return wasDragging; // Return true if was drag, false if was click
};
return { isDragging, handlePointerDown, handlePointerMove, handlePointerUp };
}Gesture state machine:
IDLE → PRESS (pointer down)
PRESS → DRAG (movement > threshold)
PRESS → CLICK (pointer up, no movement)
DRAG → DRAG_END (pointer up)
DRAG_END → IDLE (animation complete)---
Part 5: Motion & Animation
The Frequency Principle
Animation frequency should match usage frequency:
| Usage Pattern | Animation Approach |
|---|---|
| 100+ times/day | No animation—instant response |
| Occasional use | Standard animation (150-300ms) |
| Rare/first-time | Can add delight, longer duration |
Example: A "send message" button used constantly → instant. An "export report" button used weekly → can animate.
Easing Blueprint
Ease-out family (most common):
Use for entrances, user-initiated actions, and most UI transitions.
:root {
/* Increasing intensity: quad → cubic → quart → quint */
--ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);
--ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);
--ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1);
}Ease-in-out family:
Use for on-screen movement (element moving from point A to point B).
:root {
--ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);
--ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1);
}Easing decision flowchart:
Is the element entering or exiting the screen?
→ Yes: Use ease-out (for both enter AND exit)
Is the element moving on screen (A to B)?
→ Yes: Use ease-in-out
Is it a hover state or color change?
→ Yes: Use ease (CSS default) or ease-out-quad
Is it constant/looping motion (spinner, progress)?
→ Yes: Use linearTiming Guidelines
| Element Type | Duration | Notes |
|---|---|---|
| Micro-interactions | 100-150ms | Buttons, toggles, hover states |
| Tooltips, dropdowns | 150-250ms | Small UI appearing |
| Modals, drawers | 200-300ms | Larger surfaces |
| Page transitions | 300-400ms | Full view changes |
| Staggered items | 30-50ms delay | Between each item |
Important: Exit animations should be 20-30% faster than entrances.
Animation Patterns
Entrances:
- Fade in + slide up (8-16px)
- Scale from 0.95 to 1 + fade (never from 0)
- Stagger children by 30-50ms
Exits:
- Fade out (faster than entrance)
- Scale to 0.95 + fade
- Slide in direction of dismissal
Transform origin:
Always set transform-origin toward the trigger element:
/* Dropdown opening from button */
.dropdown {
transform-origin: top left; /* Opens from button location */
}
/* Modal opening from center */
.modal {
transform-origin: center center;
}Hover flicker prevention:
/* ❌ Don't animate the parent on hover */
.card:hover {
transform: scale(1.02); /* Causes flicker */
}
/* ✅ Animate a child element instead */
.card:hover .card-content {
transform: scale(1.02);
}Sequential tooltips:
After the first tooltip in a series, skip animation for subsequent ones:
// Skip animation if another tooltip was shown recently
const skipAnimation = Date.now() - lastTooltipTime < 300;Spring Physics
When to use springs:
- Drag and drop interactions
- Gesture-based animations
- Interruptible motion (user can grab mid-animation)
- Physics-based feel (natural, organic)
Spring parameters:
// Physical spring configuration
const spring = {
stiffness: 300, // Higher = faster, snappier
damping: 30, // Higher = less oscillation
mass: 1 // Higher = more inertia, slower
};
// Typical ranges:
// stiffness: 100-1000 (most UI: 200-400)
// damping: 10-100 (most UI: 20-40)
// mass: 0.5-2 (most UI: 1)Critical principle: Never reuse spring values
Each interaction should have its own tuned spring. A dropdown menu spring differs from a drag-to-dismiss spring.
// ❌ Bad - same spring for everything
const SPRING = { stiffness: 300, damping: 30 };
// ✅ Good - tuned per interaction
const DROPDOWN_SPRING = { stiffness: 400, damping: 35, mass: 0.8 };
const DRAG_SPRING = { stiffness: 250, damping: 25, mass: 1 };
const BOUNCE_SPRING = { stiffness: 180, damping: 12, mass: 1 };Damping for rubber band effects:
// Rubber band effect for over-scroll
function rubberBand(offset, limit, elasticity = 0.55) {
const clampedOffset = Math.max(0, offset);
const delta = clampedOffset - limit;
if (delta <= 0) return offset;
// Logarithmic decay for natural feel
return limit + (1 - Math.exp(-delta / (limit * elasticity))) * limit * elasticity;
}iOS-style projection (momentum scrolling):
// Project final position based on velocity
function project(velocity, position, deceleration = 0.998) {
// v(t) = v0 * deceleration^t
// When v(t) ≈ 0, t = log(0.001) / log(deceleration)
const duration = Math.log(0.001) / Math.log(deceleration);
const distance = velocity * (1 - Math.pow(deceleration, duration)) / (1 - deceleration);
return position + distance;
}Framer Motion spring shorthand:
// Simple configuration
const springConfig = {
type: "spring",
duration: 0.5, // Overall duration
bounce: 0.2 // 0 = no bounce, 1 = very bouncy
};
// Subtle bounce (most UI): 0.1 - 0.3
// Playful bounce: 0.3 - 0.5
// Avoid > 0.5 in most production UIMotion Choreography
Blur overlapping layers:
When animated elements cross paths, they create visual noise. Add subtle blur:
/* Add 1-2px blur during transitions */
.transitioning-element {
filter: blur(1px);
}
/* Or use will-change to hint GPU compositing */
.animated-layer {
will-change: transform;
transform: translateZ(0); /* Force separate layer */
}Stagger animation delays:
// Stagger children by 30-50ms each
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.04, // 40ms between each
delayChildren: 0.1, // Wait 100ms before starting
}
}
};
const item = {
hidden: { opacity: 0, y: 8 },
show: { opacity: 1, y: 0 }
};Double exit stiffness:
Exit animations should feel quicker. Double the spring stiffness:
function AnimatedPanel({ isOpen }) {
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{
enter: { type: "spring", stiffness: 300, damping: 30 },
exit: { type: "spring", stiffness: 600, damping: 30 } // 2x stiffness
}}
/>
);
}Crossfade icons (not swap):
When changing icons, don't just swap. Scale down + blur out the old, scale up + blur in the new:
// Icon crossfade
<AnimatePresence mode="wait">
<motion.div
key={iconKey}
initial={{ opacity: 0, scale: 0.5, filter: 'blur(7px)' }}
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
exit={{ opacity: 0, scale: 0.5, filter: 'blur(7px)' }}
transition={{ duration: 0.15 }}
>
<Icon />
</motion.div>
</AnimatePresence>Morph surfaces with overflow: hidden:
When morphing between shapes, prevent content from spilling:
// Container with overflow: hidden + crossfade content
<motion.div
layout
style={{ overflow: 'hidden', borderRadius: 12 }}
transition={{ layout: { duration: 0.3 } }}
>
<AnimatePresence mode="wait">
<motion.div
key={contentKey}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{content}
</motion.div>
</AnimatePresence>
</motion.div>High-Frequency Actions
No fade-in for menus:
Menus triggered frequently should appear instantly, but can fade out:
// Instant appear, animated dismiss
<motion.div
initial={false} // Skip enter animation
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0.1 } }}
>
<Menu />
</motion.div>Skip animation between rapid data updates:
// Skip animation if update is too fast
const lastUpdate = useRef(Date.now());
const ANIMATION_THRESHOLD = 100; // ms
function updateValue(newValue) {
const now = Date.now();
const shouldAnimate = now - lastUpdate.current > ANIMATION_THRESHOLD;
lastUpdate.current = now;
if (shouldAnimate) {
animateToValue(newValue);
} else {
setValueInstantly(newValue);
}
}Keyboard interactions often need no animation:
// Tab navigation - instant focus, no animation
// Arrow key navigation - instant highlight
// Enter to select - instant (or very fast 50ms)
const keyboardTransition = { duration: 0.05 };
const pointerTransition = { duration: 0.15 };
const transition = isKeyboardNav ? keyboardTransition : pointerTransition;Gesture Lifecycle
Three phases of gesture handling:
START (pointer down) → Set constraints, capture pointer
MOVE (pointer move) → Update position continuously
END (pointer up) → Animate to final positionUse jump() for continuous updates, set() for animated snap:
import { useMotionValue, useSpring } from 'framer-motion';
function DraggableElement() {
const x = useMotionValue(0);
const springX = useSpring(x, { stiffness: 300, damping: 30 });
const handleDrag = (e) => {
// During drag: jump() for instant tracking (no spring delay)
x.jump(e.clientX - startX);
};
const handleDragEnd = () => {
// On release: set() for animated snap to final position
springX.set(snapToNearest(x.get()));
};
return <motion.div style={{ x: springX }} />;
}useTransform for derived values:
import { useMotionValue, useTransform } from 'framer-motion';
function SwipeCard() {
const x = useMotionValue(0);
// Derive rotation from horizontal position
const rotate = useTransform(x, [-200, 200], [-15, 15]);
// Derive opacity from position
const opacity = useTransform(x, [-200, 0, 200], [0.5, 1, 0.5]);
return (
<motion.div
style={{ x, rotate, opacity }}
drag="x"
dragConstraints={{ left: 0, right: 0 }}
/>
);
}Gesture state tracking:
function useGestureState() {
const [state, setState] = useState('idle');
// idle → press → drag → drag-end → idle
const handlers = {
onPointerDown: () => setState('press'),
onDragStart: () => setState('drag'),
onDragEnd: () => {
setState('drag-end');
// Return to idle after animation
setTimeout(() => setState('idle'), 300);
},
onPointerUp: () => {
if (state === 'press') setState('idle'); // Was click, not drag
}
};
return { state, handlers };
}Animation Performance
Only animate compositor properties:
/* ✅ GPU-accelerated (cheap) */
transform: translateX(100px);
transform: scale(1.1);
transform: rotate(45deg);
opacity: 0.5;
/* ❌ Triggers layout/paint (expensive) */
width: 200px;
height: 200px;
top: 100px;
left: 100px;
margin: 20px;
padding: 20px;Never use `transition: all`:
/* ❌ Bad - animates everything including layout properties */
.element {
transition: all 0.3s ease;
}
/* ✅ Good - explicit properties */
.element {
transition: transform 0.3s var(--ease-out-cubic),
opacity 0.3s var(--ease-out-cubic);
}Fix transform shakiness:
/* Add will-change if animation looks shaky */
.animated-element {
will-change: transform;
}
/* Remove after animation completes to free memory */CSS vs JavaScript animations:
| Use CSS | Use JavaScript |
|---|---|
| Simple state transitions | Complex sequences |
| Hover/focus effects | Gesture-based |
| No user interaction during | Interruptible animations |
| Performance-critical | Dynamic values |
Reduced Motion
Every animation needs a reduced motion alternative:
/* Base animation */
.modal {
animation: slideIn 0.3s var(--ease-out-cubic);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(16px);
}
}
/* Reduced motion: instant or fade only */
@media (prefers-reduced-motion: reduce) {
.modal {
animation: fadeIn 0.15s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
}
}Framer Motion hook:
import { useReducedMotion } from 'framer-motion';
function Modal({ children }) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0.1 : 0.3 }}
>
{children}
</motion.div>
);
}What reduced motion should do:
- Remove parallax effects
- Stop auto-playing videos/carousels
- Replace slide/scale with fade or instant
- Keep essential feedback (success checkmarks can still appear, just not animated)
---
Part 6: Accessibility Essentials
WCAG Quick Reference
Perceivable:
- Color contrast: 4.5:1 text, 3:1 UI components
- Don't rely on color alone (add icons, patterns)
- Text resizable to 200% without loss
- Captions for video; transcripts for audio
Operable:
- All functionality via keyboard
- No keyboard traps
- Skip links for repeated content
- Touch targets: 44x44px minimum
Understandable:
- Consistent navigation
- Identify input errors clearly
- Labels and instructions for forms
Robust:
- Semantic HTML elements
- ARIA only when HTML isn't enough
- Tested with screen readers
Focus Management
Use `:focus-visible` over `:focus`:
/* ✅ Only show focus ring for keyboard navigation */
:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* ❌ Don't remove outline without replacement */
:focus {
outline: none; /* BAD - removes accessibility */
}Compound controls:
/* Highlight parent when any child is focused */
.input-group:focus-within {
box-shadow: 0 0 0 2px var(--color-primary);
}Keyboard Navigation
All interactive elements must be keyboard-operable:
// ❌ Click-only interaction
<div onClick={handleAction}>Click me</div>
// ✅ Keyboard accessible
<button onClick={handleAction}>Click me</button>
// ✅ If must use div, add keyboard support
<div
role="button"
tabIndex={0}
onClick={handleAction}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAction();
}
}}
>
Click me
</div>Keyboard patterns:
- Tab order must match visual order
- Enter/Space activate buttons and links
- Escape closes dialogs and dropdowns
- Arrow keys navigate within components (tabs, menus)
ARIA Patterns
Icon buttons require `aria-label`:
// ❌ No accessible name
<button><CloseIcon /></button>
// ✅ Accessible
<button aria-label="Close dialog"><CloseIcon /></button>Form controls require labels:
// ❌ No label
<input type="email" placeholder="Email" />
// ✅ Visible label
<label>
Email
<input type="email" />
</label>
// ✅ Or visually hidden label
<label htmlFor="email" className="sr-only">Email</label>
<input id="email" type="email" placeholder="email@example.com" />Live regions for async updates:
// Announce dynamic content to screen readers
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>Semantic HTML before ARIA:
// ❌ ARIA role when native element exists
<div role="button" tabIndex={0}>Submit</div>
// ✅ Use native element
<button>Submit</button>
// ❌ ARIA for native functionality
<div role="navigation">...</div>
// ✅ Use native element
<nav>...</nav>Common ARIA patterns:
<!-- Modal -->
<div role="dialog" aria-modal="true" aria-labelledby="modal-title">
<h2 id="modal-title">Dialog Title</h2>
</div>
<!-- Tab panel -->
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel1">Tab 1</button>
</div>
<div role="tabpanel" id="panel1">Content</div>
<!-- Loading state -->
<button aria-busy="true" aria-describedby="loading-text">
<span id="loading-text" className="sr-only">Loading...</span>
</button>---
Part 7: Performance Patterns
Virtualization
Large lists require virtualization:
// Use virtualization for lists > 50 items
import { VList } from 'virtua';
function LargeList({ items }) {
return (
<VList style={{ height: 400 }}>
{items.map(item => <ListItem key={item.id} item={item} />)}
</VList>
);
}CSS-based virtualization:
/* For simpler cases, use content-visibility */
.list-item {
content-visibility: auto;
contain-intrinsic-size: 0 60px; /* Estimated height */
}Layout Thrashing
Avoid layout reads in render:
// ❌ Bad - forces layout recalculation
function Component() {
const width = element.getBoundingClientRect().width; // Layout read
element.style.width = width + 10 + 'px'; // Layout write
const height = element.offsetHeight; // Another layout read!
}
// ✅ Good - batch reads, then writes
function Component() {
// Batch reads
const width = element.getBoundingClientRect().width;
const height = element.offsetHeight;
// Then batch writes
requestAnimationFrame(() => {
element.style.width = width + 10 + 'px';
element.style.height = height + 10 + 'px';
});
}Properties that trigger layout:
offsetHeight,offsetWidth,offsetTop,offsetLeftgetBoundingClientRect()scrollHeight,scrollWidth,scrollTop,scrollLeftgetComputedStyle()
Resource Loading
Preconnect to CDN domains:
<!-- Add in <head> for domains you'll fetch from -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://cdn.example.com" crossorigin />Preload critical fonts:
<link
rel="preload"
href="/fonts/inter-var.woff2"
as="font"
type="font/woff2"
crossorigin
/>Image loading strategy:
// Above the fold: load immediately
<img src="/hero.jpg" fetchpriority="high" />
// Below the fold: lazy load
<img src="/card.jpg" loading="lazy" />
// Critical background images: preload
<link rel="preload" as="image" href="/hero-bg.jpg" />---
Part 8: Content & Copy
Writing Style
Active voice over passive:
✅ "Install the CLI"
❌ "The CLI will be installed"
✅ "Your changes were saved"
❌ "Changes have been saved by the system"Title Case for headings and buttons:
✅ "Save API Key"
❌ "Save api key"
✅ "Getting Started"
❌ "Getting started"Use numerals:
✅ "8 deployments"
❌ "eight deployments"
✅ "3 items selected"
❌ "three items selected"Labels & Messages
Specific labels over generic:
✅ "Save API Key"
❌ "Continue"
✅ "Create Project"
❌ "Submit"
✅ "Delete Repository"
❌ "Confirm"Error messages include fix/next step:
✅ "Email must include @ symbol"
❌ "Invalid email"
✅ "Password must be at least 8 characters"
❌ "Password too short"
✅ "Could not connect. Check your internet connection and try again."
❌ "Network error"Internationalization
Use Intl APIs for formatting:
// ❌ Hardcoded format
const date = `${month}/${day}/${year}`;
const price = `$${amount.toFixed(2)}`;
// ✅ Locale-aware formatting
const date = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium'
}).format(new Date());
const price = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
// Relative time
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // "yesterday"---
Part 9: Anti-Patterns Checklist
Flag these patterns during design review:
Accessibility Violations
- [ ]
user-scalable=noormaximum-scale=1in viewport meta - [ ]
<div onClick>instead of<button>for interactive elements - [ ] Form inputs without associated labels
- [ ] Icon buttons without
aria-label - [ ]
outline: nonewithout focus replacement
Performance Issues
- [ ]
transition: all(animates layout properties) - [ ] Images without explicit
widthandheight - [ ] Large arrays (>50 items) rendered without virtualization
- [ ] Layout reads (
getBoundingClientRect) in render cycle
UX Problems
- [ ]
onPastewithpreventDefault()(blocks paste) - [ ] Hardcoded date/number formats (not using Intl)
- [ ] Placeholder used as label
- [ ] Validation on every keystroke
- [ ] No empty state designed
Mobile Issues
- [ ] Touch targets smaller than 44x44px
- [ ] No
touch-action: manipulationon buttons - [ ] Hover effects without
@media (hover: hover)query
Interface Robustness Checklist
Test every interactive component against these stress conditions:
Rapid User Input:
- [ ] Scroll fast — does it break or show visual glitches?
- [ ] Spam click — does it trigger multiple actions or crash?
- [ ] Resize window rapidly — does layout break or cause errors?
- [ ] Type very fast in inputs — does it lag or lose characters?
Animation Interruption:
- [ ] Interrupt animations mid-way — does it recover gracefully?
- [ ] Close modal while opening — does it handle state correctly?
- [ ] Navigate away during transition — does it clean up properly?
- [ ] Trigger multiple animations simultaneously — do they conflict?
Network Conditions:
- [ ] Test with slow network (3G throttling) — does it show loading states?
- [ ] Test with offline mode — does it fail gracefully?
- [ ] Test with request timeout — does it retry or show error?
Input Methods:
- [ ] Test with keyboard only — is everything accessible?
- [ ] Test with screen reader — are states announced?
- [ ] Test with touch device — are hit areas adequate?
- [ ] Test with trackpad gestures — do scroll/swipe work correctly?
State Combinations:
- [ ] Test all combinations of loading + error + empty states
- [ ] Test disabled state during async operations
- [ ] Test hover + focus + active simultaneously
- [ ] Test with maximum and minimum content
Memory & Performance:
- [ ] Run for extended period — does memory grow unbounded?
- [ ] Test with large datasets — does it virtualize or paginate?
- [ ] Monitor for memory leaks in animations and observers
- [ ] Check for event listener cleanup on unmount
---
Part 10: Design System References
Study These Systems
For Clarity & Precision:
- Linear - Information density done right
- Stripe - Trust through craft
- Vercel - Developer-focused simplicity
For Warmth & Approachability:
For Data & Density:
- Bloomberg Terminal - Maximum information
- Figma - Tool-like precision
- GitHub - Code-centric clarity
For Motion & Delight:
When Generating Variants
Reference specific aspects:
- "Use Linear's density approach"
- "Stripe's button hierarchy"
- "Airbnb's card layout"
- "Notion's toggle interaction"
- "Vercel's dark mode palette"
---
Part 11: Code Patterns Library
Practical patterns for implementing advanced interactions.
useMotionValue vs useState
Performance difference:
// ❌ Bad - triggers re-render on every frame
const [x, setX] = useState(0);
<div style={{ transform: `translateX(${x}px)` }} />
// ✅ Good - bypasses React, updates directly
const x = useMotionValue(0);
<motion.div style={{ x }} />Use useMotionValue for:
- Continuous animation values (position, scale, opacity)
- Gesture-driven values (drag position)
- Any value that changes at 60fps
Use useState for:
- Discrete states (open/closed, active tab)
- Values that trigger layout changes
useSpring: jump() vs set()
const springValue = useSpring(0, { stiffness: 300, damping: 30 });
// set() - Animate to target value with spring physics
springValue.set(100); // Smoothly animates from current → 100
// jump() - Instantly set value, no animation
springValue.jump(100); // Immediately becomes 100
// Use jump() during drag, set() on release
const handleDrag = (e) => springValue.jump(e.clientX);
const handleRelease = () => springValue.set(snapPoint);Grid Stacking for Overlapping Elements
Stack elements using CSS Grid (no absolute positioning):
// All children occupy the same grid cell
function StackedElements({ children }) {
return (
<div style={{
display: 'grid',
gridTemplateColumns: '1fr',
gridTemplateRows: '1fr',
}}>
{children.map((child, i) => (
<div key={i} style={{ gridArea: '1 / 1' }}>
{child}
</div>
))}
</div>
);
}Benefits over `position: absolute`:
- Children contribute to parent size
- Natural document flow
- Better for responsive layouts
Native Scroll vs Wheel Events
Prefer native scroll over wheel event listeners:
// ❌ Bad - janky, blocks main thread
element.addEventListener('wheel', (e) => {
scrollPosition += e.deltaY;
element.style.transform = `translateY(${-scrollPosition}px)`;
});
// ✅ Good - smooth, GPU-accelerated
<div style={{ overflow: 'auto', scrollBehavior: 'smooth' }}>
{content}
</div>When you need scroll position reactively:
// Use Intersection Observer for scroll-triggered effects
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
},
{ threshold: 0.1 }
);clip-path for Animated Resizing
Animate size without layout shifts:
// ❌ Bad - animating width triggers layout
<motion.div animate={{ width: isOpen ? 300 : 0 }} />
// ✅ Good - clip-path is GPU-accelerated
<motion.div
style={{ width: 300 }} // Fixed width
animate={{
clipPath: isOpen
? 'inset(0 0 0 0)'
: 'inset(0 100% 0 0)' // Clip from right
}}
/>Reveal patterns:
/* Reveal from left */
clip-path: inset(0 100% 0 0) → inset(0 0 0 0)
/* Reveal from center */
clip-path: inset(0 50% 0 50%) → inset(0 0 0 0)
/* Reveal from top */
clip-path: inset(0 0 100% 0) → inset(0 0 0 0)
/* Circle reveal from center */
clip-path: circle(0% at 50% 50%) → circle(100% at 50% 50%)Scroll Fading (Blur Fade Effect)
Fade content at scroll edges:
.scroll-container {
overflow-y: auto;
mask-image: linear-gradient(
to bottom,
transparent 0%,
black 10%,
black 90%,
transparent 100%
);
}
/* Or use scroll-driven animations (modern browsers) */
@supports (animation-timeline: scroll()) {
.scroll-item {
animation: fadeIn linear both;
animation-timeline: view();
animation-range: entry 0% entry 20%;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
}overflow: clip vs hidden
Use `clip` when you don't need scroll:
/* hidden: creates scroll container, may affect stacking */
.container { overflow: hidden; }
/* clip: just clips, no scroll container created */
.container { overflow: clip; }When to use each:
hidden- Need scrollable overflow (JS can scroll it)clip- Just want to hide overflow, no scroll needed (better perf)
Re-mount with React Key for CSS Keyframes
Replay CSS animations by changing key:
// CSS animation plays once on mount
function Notification({ message }) {
const [key, setKey] = useState(0);
const triggerAnimation = () => setKey(k => k + 1);
return (
<div key={key} className="animate-shake">
{message}
</div>
);
}.animate-shake {
animation: shake 0.5s ease-out;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}Smart Image Preloading
Preload images before they're needed:
// Preload on hover (link navigation)
function NavLink({ href, children }) {
const preloadImages = () => {
const images = ['/hero.jpg', '/feature.png'];
images.forEach(src => {
const img = new Image();
img.src = src;
});
};
return (
<Link
href={href}
onMouseEnter={preloadImages}
onFocus={preloadImages}
>
{children}
</Link>
);
}
// Preload based on viewport proximity
function useImagePreload(src, rootMargin = '200px') {
const ref = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
const img = new Image();
img.src = src;
observer.disconnect();
}
},
{ rootMargin }
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, [src, rootMargin]);
return ref;
}layoutId for Shared Element Transitions
Morph elements between views:
import { motion, AnimatePresence } from 'framer-motion';
function App() {
const [selected, setSelected] = useState(null);
return (
<>
{/* Grid of cards */}
{items.map(item => (
<motion.div
key={item.id}
layoutId={`card-${item.id}`}
onClick={() => setSelected(item)}
>
<Card item={item} />
</motion.div>
))}
{/* Expanded view */}
<AnimatePresence>
{selected && (
<motion.div
layoutId={`card-${selected.id}`}
className="expanded-card"
>
<ExpandedCard item={selected} />
</motion.div>
)}
</AnimatePresence>
</>
);
}Debounced Resize Observer
Prevent excessive callbacks during resize:
function useResizeObserver(ref, callback, debounceMs = 100) {
useEffect(() => {
const element = ref.current;
if (!element) return;
let timeoutId;
const observer = new ResizeObserver((entries) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
callback(entries[0].contentRect);
}, debounceMs);
});
observer.observe(element);
return () => {
clearTimeout(timeoutId);
observer.disconnect();
};
}, [ref, callback, debounceMs]);
}---
Quick Decision Framework
When unsure, ask:
1. Is it clear? → User knows what to do and what happened 2. Is it fast? → Minimum steps, appropriate feedback 3. Is it consistent? → Matches patterns elsewhere in the app 4. Is it accessible? → Keyboard, screen reader, color contrast 5. Is it calm? → No unnecessary motion, color, or elements 6. Is it specific? → Labels describe exactly what will happen 7. Is it recoverable? → User can undo or go back
Animation Decision Quick Check
Should this animate?
├── Used 100+ times/day? → No animation
├── Entering/exiting screen? → ease-out, 150-250ms
├── Moving on screen? → ease-in-out, 200-300ms
├── Hover/color change? → ease, 100-150ms
└── Unsure? → Start without animation, add if neededInteraction Pattern Quick Reference
| Situation | Solution |
|---|---|
| Overlapping motion | Add 1-2px blur during transition |
| Menu animation | Fade-out only, no fade-in (instant appear) |
| Keyboard interactions | Often no animation needed (50ms max) |
| High-frequency updates | Skip animation if update < 100ms apart |
| Drag gesture | Use jump() during, set() at end |
| Touch gestures | Use touch-action: none to capture |
| Thin hit areas | Expand with ::after pseudo-element |
| Scroll-based animation | Use native scroll, not wheel event |
| Width animation | Use clip-path instead of width |
| Replay CSS animation | Change React key prop |
| Morph between elements | Use Framer Motion layoutId |
| State during gesture | Track: idle → press → drag → drag-end |
| Overflow without scroll | Use overflow: clip |
| Exit animations | Double the spring stiffness |
| Icon swap | Crossfade with scale 0.5 + blur 7px |
| Preload data | Start on mousedown (before click) |
| Spring animation values | Never reuse — tune per interaction |
| React animation perf | Use useMotionValue, not useState |
Related skills
How it compares
Use design-lab for heuristic-grounded UX critique during agent UI work; pair with implementation skills when you need component code rather than principles.
FAQ
Which UX framework does design-lab include?
design-lab includes Jakob Nielsen's 10 Usability Heuristics in its Design Principles Reference, covering visibility of system status, consistency, user control, error prevention, and related foundations agents apply when generating or reviewing interfaces.
When should developers invoke design-lab?
design-lab should run when agents generate design variations or critique interfaces and need curated UX principles instead of ad hoc styling opinions. It anchors prompts to documented heuristics before implementation.
Is Design Lab safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.