Before You Start: Load Design Context
- Read
.agents/design-context.md(written by thedesign-contextskill). If it is missing, look for tokens intailwind.config.*or:rootCSS variables. If nothing exists, use the defaults (Primary#2563EB, Secondary#7C3AED, Accent#F59E0B, Tailwind gray neutrals, Inter for headings and body, Minimal style) and tell the user defaults were used — suggest runningdesign-context. - Map tokens to CSS variables once, then use only the variables:
--color-primary,--color-primary-light,--color-primary-dark,--color-secondary,--color-accent,--color-neutral-50…--color-neutral-900,--color-success/-warning/-error,--font-heading,--font-body,--font-mono,--radius, and the 4px-base spacing scale. - Tailwind: expose the same variables (v3
theme.extend, v4@theme) asprimary,primary-light,primary-dark,secondary,accent,neutral-*,font-heading,font-body. Theindigo-*/gray-*classes in the examples below are placeholders for these tokens. - Figma: if the user gives a Figma URL, pull it with the Figma MCP (
get_design_context,get_screenshot) and match it; it overrides the context file for that design. - Let the style archetype set radius, shadow depth, and how much of section 9 (trends) to use — Minimal and Corporate use none of it.
1. Component Design Principles (Atomic Design)
Build UI in layers. Never start with a full page — start with the smallest pieces.
Hierarchy
- Tokens — colors, spacing, radii, shadows, typography scale
- Atoms — button, input, badge, avatar, icon
- Molecules — search bar (input + button), form field (label + input + error), card header (avatar + name + timestamp)
- Organisms — navigation bar, hero section, comment thread, pricing table
- Templates — page layouts composed of organisms
- Pages — templates with real data
Rules
- Every component gets its own set of CSS custom properties scoped to its root.
- Prefer composition over configuration. A
Cardwithchildrenbeats aCardwith 15 props. - Components should be stateless by default. Lift state up.
/* Token layer — values come from .agents/design-context.md (defaults shown) */
:root {
--color-primary: #2563EB;
--color-surface: #FFFFFF;
--radius-md: 0.75rem;
--shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.05);
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--space-12: 3rem;
--space-16: 4rem;
}
Canonical spacing scale (all UI skills): 4px base — 4, 8, 12, 16, 24, 32, 48, 64px (Tailwind 1, 2, 3, 4, 6, 8, 12, 16). Don't use off-scale values.
2. Visual Hierarchy Framework
Control where the eye goes. Rank every element by importance and apply these levers:
| Lever | High Emphasis | Medium Emphasis | Low Emphasis |
|---|---|---|---|
| Size | 2rem+ heading | 1rem body | 0.75rem caption |
| Weight | 700–800 bold | 500 medium | 400 regular |
| Color | Primary or high-contrast text | Muted text (neutral-600) | Subtle text (neutral-500 — lightest gray that still passes 4.5:1 on white) |
| Spacing | More whitespace around it | Standard spacing | Tighter spacing |
| Position | Top-left or center | Mid-page | Bottom or sidebar |
The Squint Test
Blur your eyes or zoom out to 25%. If you can still identify the primary action and main heading, your hierarchy works.
Tailwind Implementation
<!-- High emphasis -->
<h1 class="text-4xl font-extrabold tracking-tight text-gray-950">
Ship faster with AI
</h1>
<!-- Medium emphasis -->
<p class="mt-4 text-lg text-gray-600 max-w-2xl">
Build production-ready components in minutes, not hours.
</p>
<!-- Low emphasis -->
<span class="text-xs text-gray-500 uppercase tracking-wide">
Updated 2 hours ago
</span>
3. Interactive States
Every interactive element needs all five states. No exceptions.
State Definitions
.btn-primary {
/* Default */
background: var(--color-primary);
color: white;
border-radius: var(--radius-md);
padding: var(--space-2) var(--space-4);
font-weight: 600;
transition: background-color 150ms ease, box-shadow 150ms ease, transform 150ms ease, filter 150ms ease;
/* Hover — subtle lift or brightness shift */
&:hover {
filter: brightness(1.1);
box-shadow: 0 4px 12px oklch(0% 0 0 / 0.15);
transform: translateY(-1px);
}
/* Active — pressed feel */
&:active {
transform: translateY(0);
filter: brightness(0.95);
box-shadow: var(--shadow-sm);
}
/* Focus — visible ring for keyboard users */
&:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Disabled — dimmed, no hover/active effects (don't add pointer-events: none — it hides the not-allowed cursor) */
&:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
box-shadow: none;
filter: none;
}
}
Tailwind Shorthand
<button class="bg-indigo-600 text-white px-4 py-2 rounded-lg font-semibold
hover:bg-indigo-500 hover:-translate-y-0.5 hover:shadow-lg
active:translate-y-0 active:bg-indigo-700
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600
disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none
transition-all duration-150">
Click me
</button>
4. Feedback Patterns
Loading States
- Skeleton screens over spinners. Show the shape of content that's coming.
- Use
motion-safe:animate-pulseon placeholder blocks, and mark the regionaria-busy="true"with an sr-only "Loading…" label. - Show loading inline near the action that triggered it.
<!-- Skeleton card -->
<div class="motion-safe:animate-pulse space-y-3" aria-busy="true">
<span class="sr-only">Loading…</span>
<div class="h-48 bg-gray-200 rounded-xl"></div>
<div class="h-4 bg-gray-200 rounded w-3/4"></div>
<div class="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
Success State
- Green accent, checkmark icon, brief confirmation text.
- Announce it with
role="status"(polite live region). Auto-dismiss after 5+ seconds, and never auto-dismiss messages that contain an action.
Error State
- Red accent, descriptive message, recovery action.
- Never just say "Error." Say what went wrong and what to do.
- Don't rely on red alone: pair it with an icon and text; use
role="alert"for errors that appear after submit.
Empty State
- Illustration or icon, explanation, primary action to fix it.
<div class="text-center py-16">
<svg class="mx-auto h-12 w-12 text-gray-400" aria-hidden="true"><!-- inbox icon --></svg>
<h3 class="mt-4 text-lg font-semibold text-gray-900">No messages yet</h3>
<p class="mt-2 text-sm text-gray-500">Start a conversation to see messages here.</p>
<button type="button" class="mt-6 bg-indigo-600 text-white px-4 py-2 rounded-lg">
New message
</button>
</div>
5. Navigation Patterns
Sidebar Navigation
- Width: 240–280px desktop, collapsible to 64px (icon-only).
- Group items by category with subtle section headers.
- Active item: filled background, bold text, left accent bar, and
aria-current="page".
Top Navigation
- Fixed/sticky at top. Height: 56–64px.
- Logo left, nav links center or left, actions right.
- Mobile: collapse to hamburger at
mdbreakpoint.
Tabs
- Use for switching views within the same context.
- Active tab: bottom border (2–3px) in primary color + bold text. Use
role="tablist"/role="tab"witharia-selectedand arrow-key navigation (or plain links if each tab is its own URL). - Never more than 7 tabs. Use dropdown overflow for more.
Breadcrumbs
- Use
/or>separator. - Current page is not a link, just text.
- Truncate middle items on long paths.
6. Form Design
Rules
- Labels above inputs — not floating, not inline. Always visible.
- One column — multi-column forms reduce completion rate.
- Group related fields with subtle section dividers.
- Inline validation — validate on blur, not on every keystroke.
- Error messages below the field, in red, with specific guidance.
- Primary action left-aligned at form bottom. Secondary action as text link.
<div class="space-y-1.5">
<label for="email" class="block text-sm font-medium text-gray-700">
Email address
</label>
<input
type="email"
id="email"
class="block w-full rounded-lg border border-gray-300 px-3 py-2
text-gray-900 placeholder:text-gray-400
focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20
transition-colors"
placeholder="you@example.com"
autocomplete="email"
aria-describedby="email-error"
/>
<!-- On error: remove `hidden` and set aria-invalid="true" on the input -->
<p id="email-error" class="text-sm text-red-600 hidden">Please enter a valid email address.</p>
</div>
7. Accessibility as Design
Accessibility is not an afterthought. It is a design constraint applied from the start.
Contrast
- Normal text: 4.5:1 minimum contrast ratio (WCAG 2.2 AA).
- Large text (≥24px regular, or ≥18.66px bold): 3:1 minimum.
- Non-text UI (input borders, icons, focus rings, chart marks): 3:1 against adjacent colors.
- Placeholder text is not a label and should still meet 4.5:1 if it carries information.
- Use oklch color space for perceptually uniform contrast adjustments.
Focus Indicators
- Never remove
outlinewithout replacing it. - Use
focus-visible(notfocus) to avoid showing rings on mouse click. - Ring: 2px solid, offset 2px, primary color, with 3:1 contrast against the background.
- Focused elements must not be hidden behind sticky headers/footers (WCAG 2.4.11) — add
scroll-padding-topequal to the header height.
Target Size
- WCAG 2.5.8 (AA): pointer targets at least 24×24 CSS px (or spaced so a 24px circle doesn't overlap a neighbour).
- Platform guidance for touch: 44×44pt (Apple HIG), 48×48dp (Material). Use 44px+ for primary touch targets.
Semantic Structure
- One
<h1>per page. - Headings in order (
h1>h2>h3). Never skip levels. - Use
<nav>,<main>,<aside>,<footer>landmarks. - Buttons for actions, links for navigation. Never
<div onclick>. aria-labelon icon-only buttons.
Motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
8. Responsive Breakpoint Strategy
Breakpoints (Tailwind defaults — canonical for all UI skills)
| Name | Width | Target |
|---|---|---|
sm |
640px | Large phones landscape |
md |
768px | Tablets |
lg |
1024px | Small laptops |
xl |
1280px | Desktops |
2xl |
1536px | Large screens |
Mobile-First Rules
- Start with the mobile layout. Add complexity at larger breakpoints.
- Stack columns on mobile, side-by-side on
md+. - Navigation becomes hamburger below
md. - Font sizes scale up:
text-2xl md:text-4xl lg:text-5xl. - Generous touch padding on mobile:
p-4, tighten on desktop:lg:p-2.
Container Queries (Modern CSS)
.card-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
9. Modern UI Trends
Glassmorphism
.glass {
background: oklch(100% 0 0 / 0.1);
backdrop-filter: blur(16px) saturate(180%);
border: 1px solid oklch(100% 0 0 / 0.15);
border-radius: 1rem;
}
Glass only works over a busy background — check text contrast against the lightest area behind it, and give a solid fallback: @supports not (backdrop-filter: blur(1px)) { .glass { background: oklch(20% 0.02 265 / 0.9); } }.
Bento Grid
.bento {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: minmax(200px, auto); /* fixed 200px rows clip long content */
gap: 1rem;
}
@media (max-width: 767px) { .bento { grid-template-columns: 1fr; } .bento .featured { grid-column: auto; grid-row: auto; } }
.bento .featured {
grid-column: span 2;
grid-row: span 2;
}
Aurora / Mesh Gradient Background
.aurora {
background: linear-gradient(135deg, oklch(85% 0.15 280), oklch(80% 0.2 320), oklch(75% 0.18 200));
background-size: 400% 400%;
animation: aurora-shift 8s ease infinite;
}
@keyframes aurora-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
10. Advanced CSS Techniques
:has() — Parent Selector
/* Highlight form group when input is focused */
.form-group:has(input:focus-visible) {
background: oklch(97% 0.01 265);
border-color: var(--color-primary);
}
/* Card changes layout when it contains an image */
.card:has(img) {
grid-template-rows: 200px 1fr;
}
Subgrid
.card-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
.card {
display: grid;
grid-template-rows: subgrid;
grid-row: span 3; /* header, body, footer align across cards */
}
Scroll-Driven Animations
/* Progressive enhancement: only where supported and motion is OK */
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.fade-in {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
}
}
@keyframes fade-in {
from { opacity: 0; transform: translateY(2rem); }
to { opacity: 1; transform: translateY(0); }
}
Verify: Render and Check
Don't hand over code you haven't looked at. After generating it:
- Screenshot it at mobile and desktop widths, e.g.
npx playwright screenshot --full-page --viewport-size=390,844 file://$PWD/index.html mobile.png, then again with--viewport-size=1440,900(or point at the dev server URL). - Open both images and check: no horizontal scroll or clipped/overlapping text, hierarchy reads at a glance, brand tokens are applied, images load at the right aspect ratio.
- Tab through once: every interactive element gets a visible focus ring, in a logical order.
- Check every state you built (hover, focus, disabled, loading, empty, error) at least once, and run a contrast check on muted text.
- Fix what you find and re-screenshot before reporting done.
Quick Reference: Design Checklist
Before shipping any UI component, verify:
- Visual hierarchy is clear (squint test passes)
- All interactive elements have hover, active, focus-visible, and disabled states
- Color contrast meets WCAG 2.2 AA (4.5:1 text, 3:1 large text and UI components)
- Targets are ≥24×24px (44px+ for primary touch targets)
- Rendered and screenshotted at 390px and 1440px (see Verify)
- Focus order is logical (tab through the page)
- Responsive at all breakpoints (320px to 1920px)
- Loading, empty, and error states are designed
- Spacing uses the 4px-base scale (4, 8, 12, 16, 24, 32, 48, 64)
- Typography scale is limited to 4–6 sizes
- Motion respects
prefers-reduced-motion - Semantic HTML is used (landmarks, headings, button vs. link)