.cursor/skills/product-craft/SKILL.md
Make a digital product feel professionally crafted. Encodes the brand-agnostic implementation knowledge that separates polished SaaS products from amateur ones — spacing systems, animation, shadows, typography, color, component states, micro-interactions, and invisible polish details. Routes to build (new design system setup), audit (polish an existing product), or deep-dive (specific domain guidance). Procedural, not declarative — every section has fill-in-the-blank fields, decision rules, worked examples from Linear/Vercel/Stripe, and near-miss counter-examples. Anchored on 4 deep-research files with 60+ sources and exact CSS/Tailwind/React values.
npx skillsauth add alexwox/genesis-template product-craftInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Encode the implementation-level craft knowledge that makes a digital product feel professionally built. This skill is brand-agnostic — it covers HOW to implement well, not WHAT values to choose. For brand-specific value decisions (your blue, your font, your easing curve), defer to brand-guide-creation.
The output is implementation guidance: filled-in design system specs, audit reports with prioritized fixes, or domain-specific code-level answers.
Slash commands: /product-craft, /polish
Apply this skill when the user asks things like:
Do NOT apply when the user is defining brand identity, positioning, or messaging — that is brand-guide-creation.
Anchor on these research files before giving guidance. They contain exact values, CSS/Tailwind code, and production examples.
outputs/research/spacing-layout-visual-rhythm.md — 4px grid, spacing scale, component padding, layout dimensions, responsive strategyoutputs/research/animation-motion-design-specifications.md — duration tiers, easing curves, entry/exit patterns, spring parameters, GPU performanceoutputs/research/shadows-typography-color-specifications.md — multi-layer shadows, type scale, line-height, font weight, color palettes, dark modeoutputs/research/micro-interactions-feedback-polish.md — button/input states, toasts, tooltips, dropdowns, skeletons, empty states, command paletteoutputs/research/confirmation-pages-celebration-ux.md — celebration UX patterns, confetti implementation spec, success states, when NOT to celebrate, accessibilityoutputs/research/product-design-engineering-craft.md — index document, 10 principles, quick-reference card, audit checklistBefore routing, gather:
| Input | Why It Matters | Default |
|---|---|---|
| Existing product or greenfield? | Routes to build vs audit | Ask |
| Brand guide exists? | Determines whether to defer value decisions | Assume no |
| Tech stack | Tailwind? CSS modules? Styled-components? React? | Assume Tailwind + React + Next.js |
| Component library | shadcn/ui? Radix primitives? Custom? | Assume shadcn/ui + TweakCN for brand adaptation |
| Primary design target | Dark mode or light mode first? | Light mode |
| Density preference | Compact (Linear-style 13px body) or comfortable (16px body)? | Comfortable |
| Specific domain question? | Routes to deep-dive if yes | No |
If the user asks about a specific domain ("how should I think about animations?"), skip to deep-dive. Otherwise, classify as build or audit.
IF user asks about a specific domain (animation, spacing, shadows, etc.):
→ deep-dive: go directly to that domain's procedure
ELIF the product does not exist yet or has no design system:
→ build: walk through all 7 domains in order, filling in the spec
ELIF the product exists and needs polish:
→ audit: run the checklist, identify gaps, prioritize fixes
Walk through all 7 domains in order. For each domain:
Output: a complete design token file (CSS custom properties or Tailwind config) plus a component state specification.
For an existing product:
Output: an audit report with scored checklist, prioritized fix list, and code-level fixes.
When the user asks about one domain:
Output: domain-specific guidance with code.
Each domain follows the same structure: procedure (fill-in-the-blank), decision rules, worked example, near-miss counter-example, anti-pattern, and reference pointer.
Before defining layout, fill in:
| Field | Your Value | |---|---| | Base unit | ___ (4px or 8px) | | Primary scale stops | ___ (list the 8-12 values your system uses) | | Button height (default) | ___ px | | Button padding (default) | ___ vertical × ___ horizontal | | Input height (default) | ___ px | | Card padding | ___ px | | Modal padding | ___ px | | Label → input gap | ___ px | | Field → field gap | ___ px | | Section → section gap | ___ px | | Content max-width (app) | ___ px | | Content max-width (prose) | ___ px | | Sidebar width (expanded) | ___ px | | Sidebar width (collapsed) | ___ px | | Header height | ___ px |
Decision rules:
gap on flex/grid containers, not margins on children.clamp() for fluid interpolation.Worked example — Linear:
| Field | Value |
|---|---|
| Base unit | 4px |
| Primary scale | 4, 8, 12, 16, 24, 32, 48, 64 |
| Button height | 36px (h-9) |
| Button padding | 8px × 16px (py-2 px-4) |
| Input height | 36-40px (h-9 to h-10) |
| Card padding | 24px (p-6) |
| Modal padding | 24px (p-6) |
| Label → input gap | 6-8px (gap-1.5 to gap-2) |
| Field → field gap | 12-16px (gap-3 to gap-4) |
| Section → section gap | 32-48px (gap-8 to gap-12) |
| Content max-width (app) | No max — fills viewport |
| Content max-width (prose) | 672px (max-w-2xl) |
| Sidebar width | 220px |
| Sidebar collapsed | 48px |
| Header height | 48px (h-12) |
Near-miss counter-example:
A product uses a proper 4px base grid and consistent spacing within components. Cards have p-6, buttons have correct padding, inputs are well-sized. But the gaps between form field groups and section gaps are both 16px — same value. The form looks like a wall of fields with no visual grouping. Users can't scan the form structure because related fields don't cluster. Fix: field-to-field gap stays at 16px, but group-to-group gap increases to 32px, creating a 2:1 proximity ratio that makes groups visually distinct.
Anti-pattern: Uniform spacing everywhere. When every gap is gap-4, nothing is grouped and nothing is separated. The page reads as a flat list regardless of content hierarchy.
Reference: outputs/research/spacing-layout-visual-rhythm.md — full component padding tables, production design system comparisons (Radix, Carbon, Atlassian, Material), Tailwind configs, responsive clamp() formulas.
Before choosing fonts and sizes, fill in:
| Field | Your Value | |---|---| | Body font | ___ | | Mono font | ___ | | Body size | ___ px | | Body line-height | ___ (unitless) | | Body max-width | ___ ch | | Heading sizes (H1/H2/H3) | ___ / ___ / ___ px | | Heading line-height | ___ (unitless) | | Heading letter-spacing | ___ em | | Weight for body text | ___ | | Weight for labels/nav | ___ | | Weight for subheadings | ___ | | Weight for titles | ___ | | Uppercase label letter-spacing | ___ em |
Decision rules:
Worked example — Linear:
| Field | Value |
|---|---|
| Body font | Inter |
| Mono font | JetBrains Mono |
| Body size | 13px (dense) |
| Body line-height | 1.4-1.5 |
| Body max-width | 65ch |
| H1 / H2 / H3 | 24px / 20px / 16px |
| Heading line-height | 1.25 |
| Heading letter-spacing | -0.02em (tracking-tight) |
| Weights | 400 (body), 500 (labels), 600 (subheadings), 700 (titles) |
| Uppercase label tracking | 0.08em |
Near-miss counter-example:
A product picks Instrument Serif for headings and Inter for body — a distinctive, well-chosen pairing that passes the brand uniqueness test. Font sizes, weights, and heading hierarchy are all correctly defined. But the guide doesn't specify max-width for body text or adjust line-height below the framework default of 1.5. On desktop, body paragraphs stretch to 120+ characters per line, making long-form content (docs, blog, settings descriptions) exhausting to read. The typography looks professional in hero sections but breaks in any context with more than two sentences. Fix: add max-width: 65ch and set line-height to 1.6 for reading-heavy contexts.
Anti-pattern: Using more than 4 font weights. When you use 300, 400, 500, 600, 700 all on one page, the hierarchy becomes muddy — nothing stands out because everything has slightly different emphasis.
Reference: outputs/research/shadows-typography-color-specifications.md (Findings 7-12) — full type scale, line-height sliding scale, weight system, letter-spacing rules, Inter vs Geist comparison, text color hierarchy.
Before choosing colors, fill in:
| Field | Your Value | |---|---| | Action color (CTAs only) | ___ hex | | Background (primary) | ___ hex | | Background (secondary) | ___ hex | | Background (tertiary) | ___ hex | | Text primary opacity | ___ % of base | | Text secondary opacity | ___ % of base | | Text tertiary opacity | ___ % of base | | Text disabled opacity | ___ % of base | | Border default opacity | ___ % of base | | Success / Warning / Error / Info | ___ / ___ / ___ / ___ hex | | Dark mode: primary target or override? | ___ |
Decision rules:
rgb(0 0 0 / 0.08) in light mode, rgb(255 255 255 / 0.08) in dark mode.Worked example — professional B2B SaaS:
| Field | Value |
|---|---|
| Action color | #4F46E5 (indigo-600) — buttons, links only |
| Background (primary) | #FFFFFF |
| Background (secondary) | #F9FAFB (gray-50) |
| Background (tertiary) | #F3F4F6 (gray-100) |
| Text primary | rgb(0 0 0 / 0.9) |
| Text secondary | rgb(0 0 0 / 0.6) |
| Text tertiary | rgb(0 0 0 / 0.4) |
| Text disabled | rgb(0 0 0 / 0.25) |
| Border | rgb(0 0 0 / 0.08) |
| Semantic | #10B981 / #F59E0B / #EF4444 / #3B82F6 |
| Dark mode | Override (light-first) |
Near-miss counter-example: A brand picks a strong, distinctive action color (deep teal) with warm gray neutrals and proper dark mode surfaces. The palette is well-designed and passes contrast checks. But the guide doesn't restrict the action color to interactive elements — designers use teal for section backgrounds, decorative borders, and icon fills. Within three months, CTAs no longer stand out on any page because teal is everywhere. Pricing page conversion drops 18% and nobody connects it to the color drift. Fix: add the explicit constraint "action color appears ONLY on clickable elements" and list specific misuse examples.
Anti-pattern: Using border-gray-300 instead of border-gray-200. The 300 weight is visually heavy and makes every card and input feel bordered/boxed. The 200 weight is subtle enough to separate without dominating. Linear recently softened all their borders for exactly this reason.
Reference: outputs/research/shadows-typography-color-specifications.md (Findings 13-16) — Radix 12-step vs Tailwind 11-step palette construction, semantic state colors with exact hex values, dark mode remapping strategy, 60-30-10 with production examples.
Before defining elevation, fill in:
| Field | Your Value | |---|---| | Shadow color variable | hsl(___ deg ___ % ___ %) | | Level 0 (base): shadow | ___ | | Level 1 (resting cards): shadow | ___ | | Level 2 (sticky headers): shadow | ___ | | Level 3 (dropdowns): shadow | ___ | | Level 4 (modals): shadow | ___ | | Dark mode surface-0 (base) | ___ hex, lightness ___ % | | Dark mode surface-1 (sidebar) | ___ hex, lightness ___ % | | Dark mode surface-2 (cards) | ___ hex, lightness ___ % | | Dark mode surface-3 (dropdowns) | ___ hex, lightness ___ % | | Dark mode surface-4 (modals) | ___ hex, lightness ___ % |
Decision rules:
rgba(0,0,0,...)) for shadows. Use a --shadow-color variable in HSL so shadows adapt when the background changes.x:1px, y:2px). Keep consistent across the page.Worked example — light mode with layered shadows:
--shadow-color: 220deg 3% 15%;
--shadow-xs: 0 1px 2px hsl(var(--shadow-color) / 0.15);
--shadow-sm: 0.5px 1px 1px hsl(var(--shadow-color) / 0.36);
--shadow-md: 1px 2px 2px hsl(var(--shadow-color) / 0.2),
2px 4px 4px hsl(var(--shadow-color) / 0.2),
3px 6px 6px hsl(var(--shadow-color) / 0.2);
--shadow-lg: 1px 2px 2px hsl(var(--shadow-color) / 0.13),
2px 4px 4px hsl(var(--shadow-color) / 0.13),
4px 8px 8px hsl(var(--shadow-color) / 0.13),
8px 16px 16px hsl(var(--shadow-color) / 0.13);
Near-miss counter-example:
A product defines a correct 5-level shadow scale with properly layered values, tinted shadow colors, and consistent direction. Cards use shadow-sm, dropdowns use shadow-lg, modals use shadow-xl. But in dark mode, the same shadow values are applied unchanged. Since dark shadows on dark backgrounds are invisible, the dropdown and modal appear to float at the same level as the card beneath them — there's no depth hierarchy. Users click through modals because they can't distinguish the overlay from the background. Fix: in dark mode, swap shadow-based elevation for surface lightness tiers where each level is 4% lighter.
Anti-pattern: Using shadow-md on cards. It's the uncanny valley of shadow — too heavy for resting state, too light for floating. Use shadow-xs or shadow-sm for resting cards, shadow-lg for floating elements. Skip shadow-md for most use cases.
Reference: outputs/research/shadows-typography-color-specifications.md (Findings 1-6) — three-layer shadow formula, Josh Comeau methodology, Tailwind defaults, elevation mapping, dark mode surface tiers, Material Design comparison.
Before defining motion, fill in:
| Field | Your Value | |---|---| | Easing (entrance) | cubic-bezier() | | Easing (exit) | cubic-bezier() | | Easing (snap/toggle) | cubic-bezier() | | Easing (overshoot/announce) | cubic-bezier() | | Duration: micro-interaction | ___ ms | | Duration: standard transition | ___ ms | | Duration: complex/page | ___ ms | | Entry animation (default) | ___ | | Entry slide distance | ___ px | | Exit animation (default) | ___ | | Spring (toggle): stiffness / damping | ___ / ___ | | Spring (modal): stiffness / damping | ___ / ___ |
Decision rules:
duration: 0).transform and opacity. These run on the GPU compositor (~0ms per frame). Width, height, margin, padding trigger layout recalculation (5-10ms per frame) and kill 120Hz.once: true. Trigger 80-100px before element enters viewport.prefers-reduced-motion. Remove spatial animations, keep opacity fades.Worked example — Linear-caliber motion tokens:
| Field | Value |
|---|---|
| Ease enter | cubic-bezier(0, 0, 0.2, 1) |
| Ease exit | cubic-bezier(0.4, 0, 1, 1) |
| Ease snap | cubic-bezier(0.12, 0, 0.08, 1) |
| Ease overshoot | cubic-bezier(0.34, 1.56, 0.64, 1) |
| Micro | 150ms |
| Standard | 200ms |
| Complex | 300ms |
| Default entrance | fade + translateY(10px), 200ms ease-out |
| Default exit | fade + translateY(-10px), 150ms ease-in |
| Spring (toggle) | stiffness 500, damping 25 |
| Spring (modal) | stiffness 300, damping 24 |
Near-miss counter-example:
A product defines a proper easing vocabulary with distinct curves for enter/exit/snap. Duration tiers are correct. Entry animations use fade+slide. The motion system looks complete. But the active/press state on buttons uses scale(0.95) — too aggressive. Every button click looks cartoonish, like a mobile game. The product feels playful when it should feel precise. Fix: scale(0.97) is the sweet spot — barely perceptible but enough to create a tactile "press" sensation. 0.98 is too subtle (users don't notice), 0.95 is too dramatic (looks like a toy).
Anti-pattern: Fade-in as the only animation, applied identically to every element on page load. Professional products vary motion by element type: cards lift on hover, lists stagger on reveal, panels slide from their origin direction, modals scale up from 0.95.
Reference: outputs/research/animation-motion-design-specifications.md — full easing curve table with cubic-bezier values, entry/exit pattern specs, spring parameter cheat sheet for 10 component types, GPU compositing guide, linear() CSS springs, production examples from Linear/Stripe/Vercel/Raycast.
Before building components, fill in the state matrix:
Every interactive element needs 6 states. For each component, specify what changes in each state.
| State | Button | Input | Toggle | |---|---|---|---| | Default | bg: ___, text: ___ | border: ___, bg: ___ | bg: ___, knob: ___ | | Hover | bg change: ___, translateY: ___ | border: ___ | — | | Active/Pressed | scale: ___, bg: ___, duration: ___ | — | — | | Focus | ring: ___, offset: ___, color: ___ | ring: ___, border: ___, glow: ___ | ring: ___, offset: ___ | | Disabled | opacity: ___, cursor: ___ | opacity: ___, cursor: ___, bg: ___ | opacity: ___, cursor: ___ | | Loading | spinner: ___, width: ___ | — | — |
Decision rules:
:focus-visible (not :focus) so rings appear only for keyboard users, never on mouse click.scale(0.97), 100ms. These are non-negotiable feel signals.ring-blue-500/20). The low-opacity ring is the difference between "form field" and "premium form field."cubic-bezier(0.175, 0.885, 0.32, 1.275) — values > 1.0 create the slight overshoot that makes it feel physical.opacity-50 + cursor-not-allowed + pointer-events-none. Never a separate "disabled" color.Additional component rules:
| Component | Key Specification |
|---|---|
| Toast | Bottom-right, 5s success / 8s error, max 3 visible, pause-on-hover. Use Sonner. |
| Tooltip | 400ms delay before show, 150ms enter, max-w-[240px], Radix. skipDelayDuration for consecutive. |
| Dropdown | Scale from 0.95, 150ms ease-out, stagger 30-50ms (max 6-8 items). Exits faster than enters. |
| Skeleton | Shimmer 1.5s cycle, background-size: 200%, sync with background-attachment: fixed. |
| Empty state | Centered, icon + headline + description + CTA. CTA creates first item. |
| Success state (inline) | Use toast pattern with success variant (green checkmark + message). For confirming routine actions (saved, sent, updated). |
| Success state (page-level) | Centered layout: checkmark animation (scale 0→1 with overshoot, 400ms) + headline + "what happens next" body + single CTA. For milestone completions (deployment, first sale, onboarding complete). |
| Confetti | canvas-confetti library (6kB, zero deps). 100–150 particles, spread 70–90, gravity 0.8–1.2, ticks 150–200 (~2.5–3.3s). Set disableForReducedMotion: true. Use brand colors (3–4 hex). Only for major milestones after significant user effort — never for routine actions, account creation, or intermediate steps. |
Success checkmark animation spec:
The checkmark animation creates a micro-satisfaction response that a static icon cannot. Two-phase sequence: background scales in (200ms), then check path draws (400ms with 200ms delay).
.success-check {
transform: scale(0);
animation: checkScale 400ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes checkScale {
0% { transform: scale(0); }
60% { transform: scale(1.15); }
100% { transform: scale(1); }
}
.success-check path {
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: checkDraw 400ms cubic-bezier(0.65, 0, 0.45, 1) 200ms forwards;
}
@keyframes checkDraw {
to { stroke-dashoffset: 0; }
}
@media (prefers-reduced-motion: reduce) {
.success-check { animation: none; transform: scale(1); }
.success-check path { animation: none; stroke-dashoffset: 0; }
}
The overshoot easing cubic-bezier(0.34, 1.56, 0.64, 1) creates a subtle bounce. The stroke-dasharray path-drawing technique creates a "writing" effect. prefers-reduced-motion fallback shows the final state immediately without animation.
Celebration decision rule: Before adding confetti or a major success animation, answer: "This celebration marks ___. The user's actual goal was ___. Are these the same?" If not, use a clean confirmation instead. Confetti on trivial actions (account creation, email verification) feels cheap and contradicts the user's actual goal. Confetti after a genuine milestone (first deployment, first sale, completing a multi-step workflow) amplifies the peak-trust moment.
Worked example — button state system (Tailwind):
Base: bg-blue-600 text-white transition-all duration-150 ease-out
Hover: hover:bg-blue-700
Active: active:scale-[0.97] active:bg-blue-800
Focus: focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
Disabled: disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none
Loading: swap children for <Loader2 className="h-4 w-4 animate-spin" />
Near-miss counter-example: A product implements all 6 button states correctly: hover darkens, press scales, focus shows a ring, disabled dims, loading shows a spinner. The button feels great. But the input fields only have default and focus states — no hover (border doesn't change), no error state (just a red text message below with no border change), no disabled styling. The buttons feel premium but the forms feel flat. Users subconsciously register the inconsistency as "unfinished." Fix: inputs need the same 6-state discipline as buttons — hover changes border to gray-400, focus adds border-blue-500 + ring-blue-500/20 glow, error adds border-red-500 + ring-red-500/20, disabled adds bg-gray-50 + opacity-60.
Anti-pattern: Using :focus instead of :focus-visible. Every mouse click shows a focus ring, which makes the UI feel janky and causes designers to remove focus rings entirely — breaking keyboard accessibility.
Reference: outputs/research/micro-interactions-feedback-polish.md — full button/input state systems with Tailwind classes, toggle bounce animation, checkbox SVG drawing, toast/Sonner setup, tooltip/Radix configuration, dropdown stagger with keyboard nav, skeleton shimmer CSS, optimistic update patterns.
These are the details that individually seem trivial but collectively create the "this feels different" reaction.
Checklist of polish details to implement:
| Detail | Specification | Tailwind/CSS |
|---|---|---|
| Border radius hierarchy | badges 4px, buttons/inputs 6px, cards 8px, modals 12px, avatars full | rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-full |
| Nested radius correction | Inner radius = outer radius - padding | If card is rounded-lg (8px) with p-4, inner button should be rounded-md (6px) |
| Scrollbar styling | 6px thin, show on hover, neutral gray | scrollbar-width: thin; scrollbar-color: hsl(0 0% 75%) transparent; |
| Text selection | Brand color at 25% opacity | ::selection { background: hsl(brand / 0.25); color: inherit; } |
| Sidebar item selection | bg-blue-50 + left border accent | border-l-2 border-blue-500 bg-blue-50 (with padding correction for border width) |
| Command palette | Cmd+K, positioned at top-[20%], cmdk library | <Command.Dialog> from cmdk |
| Keyboard shortcut hints | Right-aligned in menu items | <kbd> element, text-[10px] font-mono border |
| Cursor affordances | pointer (clickable), grab/grabbing (draggable), not-allowed (disabled) | cursor-pointer, cursor-grab active:cursor-grabbing, cursor-not-allowed |
| Error handling | Validate on blur, clear on fix | onBlur validation, re-validate on keystroke after touched |
| Progress indicators | < 300ms: nothing. 300ms-4s: skeleton/spinner. > 4s: progress bar | Delay showing any loading state by 300ms |
| Optimistic updates | For toggles, status changes, reordering. NOT for destructive or financial actions | TanStack Query onMutate/onError/onSettled pattern |
| Prefetch on hover | Start fetching before click | onMouseEnter={() => router.prefetch(href)} |
| Success checkmark | Scale 0→1 with overshoot easing + SVG path draw for checkmark. For confirming completions. | cubic-bezier(0.34, 1.56, 0.64, 1) + stroke-dasharray technique |
| Confetti (milestone only) | canvas-confetti, 100–150 particles, 2–3s, brand colors. Only on major milestones after genuine user effort. Never on routine actions. | disableForReducedMotion: true always set |
| Success sound | Optional, high-stakes completions only (payment, deployment). Muted by default, opt-in. Under 0.5s, low-mid frequency, 30–50% volume. | AudioContext API, preload on enable |
Decision rules:
box-shadow (Tailwind ring) follows border-radius. CSS outline does not. Use ring for rounded focus indicators.Near-miss counter-example:
A product implements all the visible polish: consistent radius, proper scrollbars, branded text selection, command palette with Cmd+K. It looks and feels professional. But every navigation click shows a full-page spinner for 400ms because the developer didn't add prefetch-on-hover to links. Users navigate 50 times per session; 50 × 400ms = 20 seconds of cumulative spinner time. The product feels slow despite looking polished. Fix: add prefetch={true} or onMouseEnter prefetch to navigation links — the 200-400ms hover-to-click window is enough to preload most pages.
Anti-pattern: Hiding scrollbars entirely with overflow: hidden or scrollbar-width: none. Users need to know content is scrollable. Use thin, overlay-style scrollbars that appear on hover — not invisible scrollbars.
Reference: outputs/research/micro-interactions-feedback-polish.md (Findings 12-22) — border radius hierarchy, focus ring accessibility, cursor mapping, scrollbar CSS, text selection, empty state templates, inline validation patterns, command palette implementation with cmdk, sound design, the "100 small things" checklist, performance thresholds.
Run this checklist against any product. Score each item Pass / Partial / Fail. Prioritize fixes by impact tier.
scale(0.97) + 100ms:focus-visible ring on all interactive elements (test with Tab key)--shadow-color variable::selection)prefers-reduced-motion is respected (including confetti/celebration animations)Do not finalize any design system or audit until:
Avoid these — they are the specific things that make products feel amateur:
| Anti-Pattern | What It Looks Like | Fix |
|---|---|---|
| Uniform spacing | Every gap is gap-4, nothing grouped or separated | Use 3+ spacing tiers with 2:1 proximity ratio |
| Single-layer shadows | box-shadow: 0 4px 8px rgba(0,0,0,0.1) — flat, blurry gray | Use 3-layer formula: contact + key + fill with doubling blur |
| Pure black shadows | rgba(0,0,0,0.X) shadows on colored backgrounds | Use hsl(var(--shadow-color) / opacity) with tinted color |
| Fade-in only motion | Every element fades in identically on page load | Vary by type: cards lift, lists stagger, panels slide from origin |
| :focus instead of :focus-visible | Focus rings on every mouse click | Switch to :focus-visible for keyboard-only rings |
| shadow-md on cards | Uncanny valley shadow — too heavy for rest, too light for float | shadow-xs/shadow-sm for resting, shadow-lg for floating |
| border-gray-300 | Heavy borders that make every element feel boxed | border-gray-200 — subtle separation without domination |
| Animating layout properties | Transitions on width, height, margin, padding | Animate only transform and opacity (GPU compositor) |
| Full-page spinner | Loading overlay that blocks all content | Skeleton screens, optimistic updates, prefetch-on-hover |
| Missing loading delay | Showing spinner for 50ms loads (flash of loading) | Delay loading indicators by 300ms |
| scale(0.95) button press | Cartoonish bounce on every click | scale(0.97) — barely perceptible but tactile |
| No prefers-reduced-motion | Vestibular-disorder users get motion-sick | Wrap animations in @media (prefers-reduced-motion: no-preference) |
| Unstyled shadcn/ui | Default shadcn components with zero brand adaptation = the 2025 "vibe-coded" look | Use TweakCN to restyle shadcn components to your brand tokens before building |
| Confetti on trivial actions | Confetti on account creation, email verification, or minor actions — feels cheap and manufactured | Reserve confetti for major milestones after genuine user effort; use clean confirmation for everything else |
| Celebration in serious contexts | Playful animations in banking, healthcare, insurance — tone-deaf and trust-eroding | Use quiet reassurance: checkmark + clear confirmation text + helpful next action |
Start with shadcn/ui + TweakCN. shadcn/ui gives you accessible, well-structured Radix-based components with sensible defaults. TweakCN lets you remap those defaults to your brand's color, radius, spacing, and shadow tokens — so you get the accessibility and keyboard-nav for free while making the visual layer yours.
The workflow:
This avoids the two failure modes: (a) building everything from scratch (slow, accessibility bugs) and (b) using shadcn/ui unmodified (looks like every other AI-generated product).
brand-guide-creationbrand-guide-creation's visual system proceduresconstructing-a-landing-page skill handles section structure; this skill handles the craft layerconstructing-onboarding handles flow design; this skill handles component-level polishconstructing-a-funnel handles conversion architecture; this skill handles implementation qualityResearch files anchoring this skill (in outputs/research/):
| File | Lines | Domain |
|---|---|---|
| spacing-layout-visual-rhythm.md | 792 | Spacing, layout, component padding, responsive strategy |
| animation-motion-design-specifications.md | 1161 | Motion, easing, springs, entry/exit patterns, GPU performance |
| shadows-typography-color-specifications.md | 827 | Shadows, type scale, color palettes, dark mode |
| micro-interactions-feedback-polish.md | 1052 | Component states, toasts, tooltips, skeletons, polish details |
| confirmation-pages-celebration-ux.md | — | Celebration UX patterns, confetti implementation, success states, when NOT to celebrate |
| product-design-engineering-craft.md | — | Index document with 10 principles and quick-reference card |
Primary external sources (selected):
linear() CSS springstools
Translate role-based organizations into workflow-based organizations by decomposing roles into scored tasks, extracting dark playbooks (proprietary tacit knowledge), formalizing workflows, calculating automation ROI, and producing a sequenced automation roadmap. Use when a company wants to identify what work can be automated, extract undocumented expert knowledge, or build an automation strategy.
development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
development
Builds stakeholder-friendly project status updates from markdown sources. Use when asked for progress reports, implementation status, future plans, UI/UX flow summaries, infrastructure/data-flow summaries, risks, code smells, or scout-principle improvement notes.
development
Repeatable playbook for finding and interviewing key stakeholders to validate an offer pillar hypothesis. Produces a pain proximity map, target list, outreach plan, interview protocol, and structured synthesis of findings. Use when a hypothesis needs human validation before building.