generated/copilot/skills/design/SKILL.md
Use when building dashboards, SaaS UIs, admin interfaces, or any interface needing polished professional design. Covers design direction, craft principles, and 9-phase implementation. Triggers on: 'use design mode', 'design system', 'design system upgrade'. Full access mode.
npx skillsauth add mcouthon/agents designInstall 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.
A comprehensive guide to premium UI design and implementation. Part 1 establishes design direction, Part 2 covers craft principles, and Part 3 provides the 9-phase implementation approach.
Before writing any code, commit to a design direction. Don't default. Think about what this specific product needs to feel like.
SaaS ≠ Marketing. A SaaS product isn't a landing page. Marketing sites sell with visuals and emotion. Product interfaces serve with clarity and function. Resist the urge to make things "pop"—make them work. Glass morphism, gradients, and glow effects are spices, not the meal. The meal is information hierarchy, fast interactions, and clear feedback.
Before choosing a personality or color palette, answer these questions:
Design direction flows FROM these answers. If you can't articulate the intent, you're decorating, not designing.
Intent → Structure → Surface: Get the information architecture right first. Then establish visual hierarchy. Only then choose personality and aesthetics.
Enterprise/SaaS UI has more range than you think:
| Personality | Characteristics | Examples | | ---------------------------- | -------------------------------------------------- | ------------------------------- | | Precision & Density | Tight spacing, monochrome, information-forward | Linear, Raycast, terminal tools | | Warmth & Approachability | Generous spacing, soft shadows, friendly colors | Notion, Coda | | Sophistication & Trust | Cool tones, layered depth, financial gravitas | Stripe, Mercury | | Boldness & Clarity | High contrast, dramatic negative space, confident | Vercel | | Utility & Function | Muted palette, functional density, clear hierarchy | GitHub, developer tools | | Data & Analysis | Chart-optimized, technical but accessible | Analytics, BI tools |
Pick one. Or blend two. But commit.
| Personality | Border Radius | Shadows | Spacing | Colors | | ------------------------ | ------------- | ------------ | ------- | ------------- | | Precision & Density | 4-6px | Borders only | 8-12px | Monochrome | | Warmth & Approachability | 12-16px | Soft shadows | 16-24px | Warm neutrals | | Sophistication & Trust | 8-12px | Layered | 16-20px | Cool slate | | Boldness & Clarity | 8px | Dramatic | 24-32px | Pure B&W | | Utility & Function | 4-6px | Minimal | 8-12px | Muted palette | | Data & Analysis | 6-8px | Subtle | 12-16px | Chart-ready |
Use these as starting points—adapt to your specific context.
Don't default to warm neutrals. Consider the product:
Light or dark? Dark feels technical, focused, premium. Light feels open, approachable, clean.
Accent color — Pick ONE that means something. Blue for trust. Green for growth. Orange for energy. Violet for creativity.
The content should drive the layout:
These apply regardless of design direction. This is the quality floor.
All spacing uses a 4px base grid:
| Value | Usage |
| ------ | ------------------------------------- |
| 4px | Micro spacing (icon gaps) |
| 8px | Tight spacing (within components) |
| 12px | Standard spacing (related elements) |
| 16px | Comfortable spacing (section padding) |
| 24px | Generous spacing (between sections) |
| 32px | Major separation |
44px minimum hit area for all interactive elements. This isn't just mobile—it's motor accessibility and Fitts's Law. Small targets slow everyone down.
cursor: pointer on everything clickable. No exceptions. If a user can click it, the cursor must change. Buttons, links, cards, tabs, toggles, chips, dropdown triggers—all get cursor-pointer.
TLBR must match. If top padding is 16px, left/bottom/right must also be 16px.
/* Good */
padding: 16px;
padding: 12px 16px; /* Only when horizontal needs more room */
/* Bad */
padding: 24px 16px 12px 16px;
Stick to the 4px grid. Sharper corners feel technical, rounder corners feel friendly:
Don't mix systems. Consistency creates coherence.
Match your depth approach to your design direction. Choose ONE:
Borders-only (flat) — Clean, technical, dense. Works for utility-focused tools. Linear, Raycast use almost no shadows—just subtle borders.
Subtle single shadows — Soft lift without complexity: 0 1px 3px rgba(0,0,0,0.08)
Layered shadows — Rich, premium, dimensional. Stripe and Mercury use this approach.
Surface color shifts — Background tints establish hierarchy without shadows. A card at #fff on #f8fafc already feels elevated.
/* Borders-only approach */
--border: rgba(0, 0, 0, 0.08);
border: 0.5px solid var(--border);
/* Single shadow approach */
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
/* Layered shadow approach */
--shadow-layered:
0 0 0 0.5px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.04),
0 2px 4px rgba(0, 0, 0, 0.03), 0 4px 8px rgba(0, 0, 0, 0.02);
The craft is in the choice, not the complexity.
Glass, not glow. Glass morphism is a depth technique—frosted layers that establish hierarchy through translucency and blur. It's NOT about making things shiny. Use glass to separate content layers (sidebar, modals, floating panels). If glass doesn't serve a structural purpose, don't use it. A solid bg-card is often better than a blurred panel.
Monotonous card layouts are lazy design. A metric card doesn't have to look like a plan card doesn't have to look like a settings card.
Design each card's internal structure for its specific content—but keep the surface treatment consistent: same border weight, shadow depth, corner radius, padding scale, typography.
UI controls deserve container treatment. Date pickers, filters, dropdowns should feel like crafted objects.
Never use native form elements for styled UI. Native <select>, <input type="date"> render OS-native controls that cannot be styled. Build custom components instead.
Custom select triggers must use display: inline-flex with white-space: nowrap to keep text and chevron icons on the same row.
Numbers, IDs, codes, timestamps belong in monospace. Use tabular-nums for columnar alignment. Mono signals "this is data."
Use Phosphor Icons (@phosphor-icons/react). Icons clarify, not decorate—if removing an icon loses no meaning, remove it.
Give standalone icons presence with subtle background containers.
Build a four-level system: foreground (primary) → secondary → muted → faint. Use all four consistently.
Gray builds structure. Color only appears when it communicates: status, action, error, success. Decorative color is noise.
Ask whether each use of color is earning its place. Score bars don't need to be color-coded by performance—a single muted color works.
Every design decision should survive four tests. If a choice is just "what the framework gave me," it's not a decision—it's a default.
The Swap Test: Could you swap this component with any other app's version and nobody would notice? If yes, you haven't designed it—you've assembled it.
The Squint Test: Squint at the screen. Can you still see the hierarchy? If everything blurs into the same gray mass, your visual hierarchy is broken. Primary actions, key data, and navigation should remain distinct even when blurred.
The Signature Test: Cover the logo. Can you tell which product this is? If not, the design has no point of view. At least ONE element should be distinctive to this product.
The Token Test: Is every value (color, spacing, radius, shadow) coming from your design tokens? If you're using arbitrary values (mt-[13px], text-[#3a3a3a]), you're creating inconsistency. Every value should trace back to the system.
Also question: Is the grid alignment consistent? Is the depth strategy coherent across every surface?
Screens need grounding. A data table floating in space feels like a component demo, not a product. Consider including:
When building sidebars, consider using the same background as the main content area. Tools like Supabase, Linear, and Vercel rely on a subtle border for separation rather than different background colors.
Borders over shadows — Shadows are less visible on dark backgrounds. Lean more on borders for definition. A border at 10-15% white opacity might look nearly invisible but it's doing its job.
Adjust semantic colors — Status colors (success, warning, error) often need to be slightly desaturated for dark backgrounds.
Same structure, different values — The hierarchy system (foreground → secondary → muted → faint) still applies, just with inverted values.
When using shadcn/ui, Radix, or similar libraries, this design system overlays on top. Apply the design tokens (colors, shadows, spacing) to the library's components via CSS variables and className overrides. Don't fight the library's accessibility patterns—enhance their visual layer.
Design system work is foundational. Skip phases or do them out of order, and you'll create technical debt. Each phase builds on the previous.
| Phase | Name | What It Establishes | | ----- | -------------------- | ---------------------------------------- | | 1 | Typography | Font choice, scale, tracking, smoothing | | 2 | Color System | HSL-based colors, semantic tokens, glass | | 3 | Shadows & Elevation | Layered shadows, glow effects, depth | | 4 | Animation System | Hooks, keyframes, timing, stagger | | 5 | Core Components | Button, Input, Select, Modal redesign | | 6 | Layout Components | Sidebar, Header, Card variants | | 7 | Domain Components | Feature-specific polish (chat, forms) | | 8 | Data Display | Tables, charts, KPIs, dashboards | | 9 | Pages & Final Polish | Headers, responsive, accessibility |
Before marking any component done, verify:
cursor: pointerSkip nothing. These 6 checks catch 80% of polish issues.
Example checkpoint for a Button component:
Intent: "Triggers primary user actions with clear visual hierarchy." States: default, hover (-translate-y-px), active (translate-y-0), focus (ring), disabled (opacity-50), loading (spinner). Touch: h-11 = 44px. Cursor: cursor-pointer.
Typography sets the personality. Get this right first.
--font-family with proper fallback stackfontFamily.sans using the new font-webkit-font-smoothing: antialiased| Personality | Font Choice | Notes | | -------------- | ----------- | ----------------------------------- | | Premium/Modern | Poppins | Geometric, clean, distinct | | Technical | Geist | Sharp, developer-focused | | Approachable | Inter | Highly readable, neutral | | Native | System UI | Fast, invisible, familiar | | Data-focused | Roboto Mono | For code/data with proportional mix |
// tailwind.config.js — representative entries, extend for your full scale
fontSize: {
display: ["1.5rem", { lineHeight: "2rem", letterSpacing: "-0.025em", fontWeight: "600" }],
title: ["1.125rem", { lineHeight: "1.75rem", letterSpacing: "-0.015em", fontWeight: "600" }],
body: ["0.875rem", { lineHeight: "1.25rem" }],
label: ["0.8125rem", { lineHeight: "1.125rem", letterSpacing: "0.01em", fontWeight: "500" }],
caption: ["0.75rem", { lineHeight: "1rem" }],
}
-0.025em)0.01em)-0.01em to -0.02em)Convert to HSL-based colors that support opacity modifiers and glass morphism.
H S L (no hsl() wrapper, no alpha)--background, --foreground, --primary, --muted--glass-border, --glass-bg<alpha-value> support/* CSS: Define as H S L values only */
:root {
--background: 0 0% 100%;
--foreground: 222 47% 11%;
--primary: 211 100% 50%; /* Apple Blue */
--muted: 210 40% 96%;
--muted-foreground: 215 16% 47%;
}
.dark {
--background: 220 16% 4%;
--foreground: 210 40% 96%;
--primary: 211 100% 50%;
--muted: 220 16% 10%;
--muted-foreground: 215 20% 55%;
}
// Tailwind: Use with <alpha-value> for opacity modifiers
colors: {
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
primary: "hsl(var(--primary) / <alpha-value>)",
}
This enables bg-primary/80 → hsl(211 100% 50% / 0.8)
Opacity as a design tool. HSL with <alpha-value> gives you a powerful layering mechanism. Use opacity modifiers (/80, /60, /40) to create depth without adding new colors. A bg-primary/10 background is more cohesive than a custom light-blue—it automatically adapts to theme changes.
/* Light mode: black tint for glass */
--glass-border: 0 0% 0% / 0.06;
--glass-bg: 0 0% 0% / 0.02;
/* Dark mode: white tint for glass */
--glass-border: 0 0% 100% / 0.06;
--glass-bg: 0 0% 100% / 0.03;
| Token | Purpose |
| ---------------------------- | --------------------------- |
| background | Page/app background |
| foreground | Primary text |
| primary | Brand/action color |
| muted | Subtle backgrounds |
| muted-foreground | Secondary text |
| card | Card/elevated surfaces |
| border | Default borders |
| accent | Highlight color (if needed) |
| success/warning/error/info | Semantic states |
See Contrast Hierarchy in Part 2 for the four-level text hierarchy system.
Dark mode color strategy: Don't just invert—adjust. Dark backgrounds need lower-contrast text for the muted levels and slightly more saturated accent colors to maintain visual impact.
Note: Your depth strategy here should match your design direction choice from Part 1. Precision/Density → borders-only, Sophistication/Trust → layered shadows.
Shadows create depth and hierarchy. A comprehensive shadow system is essential for polish.
Craft note: The difference between amateur and premium shadow work is subtlety. If you can obviously see a shadow, it's probably too heavy. Shadows should be felt, not seen—they create spatial relationships without drawing attention to themselves.
/* Ambient — ultra-subtle background depth */
--shadow-ambient: 0 1px 2px 0 rgba(0, 0, 0, 0.02);
--shadow-ambient-md: 0 2px 4px 0 rgba(0, 0, 0, 0.04);
/* Card — for card components */
--shadow-card: 0 1px 2px 0 rgba(0, 0, 0, 0.02);
--shadow-card-hover: 0 4px 12px rgba(0, 0, 0, 0.06);
/* Glass — for glass morphism */
--shadow-glass: 0 4px 16px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.02);
--shadow-glass-lg: 0 8px 32px rgba(0, 0, 0, 0.06);
/* Elevated — floating elements */
--shadow-elevated:
0 8px 24px rgba(0, 0, 0, 0.08), 0 4px 8px rgba(0, 0, 0, 0.04);
--shadow-elevated-lg: 0 16px 48px rgba(0, 0, 0, 0.1);
/* Glow — colored for interactive elements */
--shadow-glow-blue: 0 4px 16px rgba(0, 113, 227, 0.15);
--shadow-glow-blue-lg: 0 8px 32px rgba(0, 113, 227, 0.2);
--shadow-glow-green: 0 4px 16px rgba(16, 185, 129, 0.15);
--shadow-glow-red: 0 4px 16px rgba(239, 68, 68, 0.15);
/* Inset — pressed states */
--shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.06);
Dark mode needs different shadow treatment:
.dark {
--shadow-card: 0 1px 2px 0 rgba(0, 0, 0, 0.2);
--shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.4);
--shadow-glow-blue: 0 4px 24px rgba(0, 113, 227, 0.25);
}
Animations make interfaces feel alive. Build a library of reusable animations.
| Token | Duration | Usage |
| -------- | -------- | --------------------------------- |
| fast | 150ms | Micro-interactions (hover, focus) |
| base | 200ms | Component transitions |
| smooth | 250ms | Standard animations |
| slow | 350ms | Page transitions, modals |
| slower | 400ms | Complex orchestrated sequences |
transitionTimingFunction: {
smooth: "cubic-bezier(0.25, 1, 0.5, 1)", // General purpose
apple: "cubic-bezier(0.25, 1, 0.5, 1)", // Apple-style
spring: "cubic-bezier(0.22, 1, 0.36, 1)", // Springy feel
"ease-out-expo": "cubic-bezier(0.19, 1, 0.22, 1)", // Dramatic slowdown
}
keyframes: {
"fade-in": {
"0%": { opacity: "0" },
"100%": { opacity: "1" },
},
"fade-slide-up": {
"0%": { opacity: "0", transform: "translateY(8px)" },
"100%": { opacity: "1", transform: "translateY(0)" },
},
"scale-in": {
"0%": { opacity: "0", transform: "scale(0.95)" },
"100%": { opacity: "1", transform: "scale(1)" },
},
"modal-enter": {
"0%": { opacity: "0", transform: "scale(0.96) translateY(8px)" },
"100%": { opacity: "1", transform: "scale(1) translateY(0)" },
},
"shimmer": {
"0%": { transform: "translateX(-100%)" },
"100%": { transform: "translateX(100%)" },
},
}
Create a hooks/use-animations.ts file with reusable hooks.
export function useInView<T extends HTMLElement = HTMLDivElement>(
options: {
threshold?: number;
rootMargin?: string;
triggerOnce?: boolean;
} = {},
): [RefObject<T | null>, boolean] {
const { threshold = 0.1, rootMargin = "0px", triggerOnce = true } = options;
const ref = useRef<T>(null);
const [isInView, setIsInView] = useState(false);
const hasTriggered = useRef(false);
useEffect(() => {
const element = ref.current;
if (!element || (triggerOnce && hasTriggered.current)) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsInView(true);
if (triggerOnce) {
hasTriggered.current = true;
observer.disconnect();
}
} else if (!triggerOnce) {
setIsInView(false);
}
},
{ threshold, rootMargin },
);
observer.observe(element);
return () => observer.disconnect();
}, [threshold, rootMargin, triggerOnce]);
return [ref, isInView];
}
Implement these as needed. Follow the useInView pattern above.
| Hook | Purpose | Key Detail |
| ------------------------- | ---------------- | -------------------------------------------------------------------------------------------------- |
| useStagger | Cascading delays | Returns getStaggerClass(index) / getStaggerStyle(index) with configurable base + stagger delay |
| useCountUp | Animated numbers | requestAnimationFrame + easeOutExpo easing, configurable duration/decimals |
| useAnimationState | Mount/unmount | Tracks entering → entered → exiting → exited phases with configurable durations |
| usePrefersReducedMotion | Accessibility | Listens to prefers-reduced-motion media query, returns boolean |
Generate .stagger-1 through .stagger-12 CSS classes with 50ms increments. Include .stagger-item base class with opacity: 0; animation-fill-mode: forwards; and a defensive .stagger-item:not([class*="animate-"]) { opacity: 1; } override.
Usage with the useStagger hook:
function ItemList({ items }: { items: Item[] }) {
const { getStaggerClass } = useStagger();
return (
<ul>
{items.map((item, i) => (
<li
key={item.id}
className={cn(
"stagger-item animate-fade-slide-up",
getStaggerClass(i),
)}
>
{item.name}
</li>
))}
</ul>
);
}
| Class | Styles |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| .elevation-card | shadow-card transition-all duration-200 → hover: shadow-card-hover -translate-y-0.5 |
| .elevation-interactive | shadow-card transition-all duration-200 → hover: shadow-glow-blue -translate-y-1 |
| .elevation-glass | shadow-glass transition-all duration-200 → hover: shadow-glass-lg -translate-y-0.5 |
| .elevation-primary | shadow-glow-blue transition-all duration-200 → hover: shadow-glow-blue-lg -translate-y-px → active: translate-y-0 |
| .elevation-float | shadow-elevated / .elevation-float-lg: shadow-elevated-lg |
With the foundation in place, redesign core UI primitives. Run the Per-Component Checkpoint after completing each component below.
const variantStyles = {
primary: cn(
"bg-primary text-primary-foreground",
"shadow-glow-blue hover:shadow-glow-blue-lg",
"hover:bg-primary/90 hover:-translate-y-px",
"active:translate-y-0 active:shadow-glow-blue",
),
secondary: cn(
"bg-card/60 backdrop-blur-sm text-foreground",
"border border-glass-border hover:border-glass-border-hover",
"shadow-card hover:shadow-card-hover hover:-translate-y-px",
),
ghost: cn(
"text-foreground-secondary",
"hover:bg-muted hover:text-foreground",
),
danger: cn(
"bg-error text-white",
"shadow-glow-red hover:shadow-glow-red",
"hover:bg-error/90 hover:-translate-y-px",
"active:translate-y-0",
),
};
// Usage
<button
className={cn(
"inline-flex items-center justify-center font-medium cursor-pointer",
"transition-all duration-250 ease-apple",
"focus-visible:ring-2 focus-visible:ring-primary/50",
variantStyles[variant],
)}
/>;
Define a consistent size scale. All sizes must maintain 44px minimum touch target:
const sizeStyles = {
sm: "h-8 px-3 text-caption rounded-lg gap-1.5", // Visual 32px, pad to 44px hit area
md: "h-10 px-4 text-body rounded-xl gap-2", // 40px, near target
lg: "h-11 px-5 text-body rounded-xl gap-2", // 44px, meets target
xl: "h-12 px-6 text-label rounded-xl gap-2.5", // 48px, generous
};
const baseInputStyles = cn(
"w-full px-3.5 py-2.5 text-body rounded-xl",
"bg-muted/30 border border-glass-border",
"text-foreground placeholder:text-foreground-muted",
"transition-all duration-200 ease-apple",
"hover:border-glass-border-hover hover:bg-muted/40",
"focus:outline-none focus:border-primary/40 focus:ring-1 focus:ring-primary/20 focus:bg-card",
"disabled:bg-muted disabled:text-foreground-muted disabled:cursor-not-allowed",
);
const errorStyles = "border-error/50 focus:border-error/60 focus:ring-error/20";
Ensure inputs have a minimum height of 44px (h-11 or py-2.5 with text) for touch target compliance.
import { ChevronDown } from "lucide-react";
<div className="relative">
<select
className={cn(
baseInputStyles,
"appearance-none pr-10", // Hide native arrow, add padding for icon
)}
>
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-foreground-muted pointer-events-none" />
</div>;
Key techniques for custom checkboxes:
sr-only input + visible proxy div for full styling controlpeer-checked:bg-primary peer-checked:border-primary for checked statepeer-focus-visible:ring-2 peer-focus-visible:ring-primary/30 for keyboard a11yscale-75 → scale-100 transition on checkcursor-pointer on the wrapping <label>Modals need three things: backdrop blur for depth, entrance animation for presence, and focus trapping for accessibility. Always trap focus inside the modal and close on Escape key.
// Backdrop
<div className={cn(
"fixed inset-0 bg-black/60 backdrop-blur-sm",
"animate-backdrop-enter",
)} />
// Content
<div className={cn(
"bg-card/90 backdrop-blur-xl border border-glass-border",
"rounded-2xl shadow-elevated-lg",
"animate-modal-enter",
)} />
Layout components establish the overall feel of the application. These should reflect your design direction from Part 1—the sidebar, header, and cards set the tone for every page.
const variants = {
default: "bg-card border border-border rounded-lg shadow-card",
elevated: cn(
"bg-card/70 backdrop-blur-xl border border-glass-border rounded-2xl",
"shadow-elevated hover:shadow-elevated-lg hover:-translate-y-0.5",
),
glass: cn(
"bg-card/60 backdrop-blur-xl border border-glass-border rounded-2xl",
"shadow-glass hover:shadow-glass-lg hover:-translate-y-0.5",
),
interactive: cn(
"bg-card/70 backdrop-blur-xl border border-glass-border rounded-2xl",
"shadow-card cursor-pointer",
"hover:shadow-glow-blue hover:border-primary/20 hover:-translate-y-1",
),
};
text-blue-400, text-cyan-400const iconColors: Record<string, string> = {
Dashboard: "text-blue-400",
Chat: "text-cyan-400",
Insights: "text-amber-400",
"Saved Items": "text-emerald-400",
Reports: "text-violet-400",
Settings: "text-slate-400",
};
<NavLink
to={item.href}
className={cn(
"flex items-center gap-3 rounded-lg py-2.5 text-label cursor-pointer",
"transition-all duration-200 ease-apple",
isActive
? cn(
"bg-primary/10 text-foreground font-semibold",
"border-l-2 border-primary pl-[10px] pr-3",
"shadow-blue",
)
: cn(
"text-muted-foreground px-3",
"hover:bg-muted/50 hover:text-foreground",
),
)}
>
<item.icon
className={cn(
"h-5 w-5 transition-colors duration-200",
isActive ? iconColors[item.name] : "text-muted-foreground",
)}
/>
{item.name}
</NavLink>
For logo marks: Use bg-gradient-to-br with primary color and shadow-glow-blue for a glowing logo mark. For gradient text: bg-clip-text text-transparent bg-gradient-to-r. For ambient glow behind navigation: use a ::before pseudo-element with radial-gradient(circle, hsl(var(--primary)), transparent 70%) at low opacity (0.03) with blur(100px).
<header
className={cn(
"sticky top-0 z-40 h-14 flex items-center px-6",
"bg-background/80 backdrop-blur-xl",
"border-b border-border",
)}
>
<div className="flex items-center justify-between w-full">
<h1 className="text-label font-semibold">{title}</h1>
<div className="flex items-center gap-2">{/* Actions */}</div>
</div>
</header>
Apply the design system to feature-specific components. Each component here should pass the Anti-Default Tests from Part 2—domain components are where products become generic if you're not careful.
// User message — gradient with glow
<div className="max-w-[85%] bg-gradient-to-br from-primary to-primary/90 text-white px-4 py-3 rounded-2xl shadow-glow-blue">
{content}
</div>
// AI/System message — glass morphism
<div className="glass rounded-2xl shadow-card min-w-[300px] max-w-[800px]">
{content}
</div>
function EmptyState({
icon: Icon,
title,
description,
suggestions,
onSuggestionClick,
}: Props) {
return (
<div className="flex-1 flex items-center justify-center p-8">
<div className="max-w-md text-center animate-fade-slide-up">
<div className="mx-auto mb-6 h-16 w-16 rounded-2xl bg-gradient-to-br from-primary to-primary/60 flex items-center justify-center shadow-glow-blue">
<Icon className="h-8 w-8 text-white" />
</div>
<h2 className="text-title text-foreground mb-2">{title}</h2>
<p className="text-body text-muted-foreground mb-8">{description}</p>
{suggestions && (
<div className="space-y-2">
{suggestions.map((s, i) => (
<button
key={i}
onClick={() => onSuggestionClick?.(s)}
className="w-full text-left px-4 py-3 rounded-xl text-body glass hover:bg-card/60 transition-all duration-200 hover:shadow-card hover:-translate-y-px cursor-pointer"
>
"{s}"
</button>
))}
</div>
)}
</div>
</div>
);
}
For chat or AI-response loading states:
// Keyframe for tailwind.config.js
"typing-dot": {
"0%, 60%, 100%": { opacity: "0.3", transform: "translateY(0)" },
"30%": { opacity: "1", transform: "translateY(-4px)" },
}
Render 3 dots with staggered animationDelay (0ms, 150ms, 300ms). Use h-2 w-2 rounded-full bg-primary animate-typing-dot.
.scroll-shadow-top {
box-shadow: inset 0 24px 16px -16px hsl(var(--background) / 0.8);
}
.scroll-shadow-bottom {
box-shadow: inset 0 -24px 16px -16px hsl(var(--background) / 0.8);
}
.scroll-shadow-both {
box-shadow:
inset 0 24px 16px -16px hsl(var(--background) / 0.8),
inset 0 -24px 16px -16px hsl(var(--background) / 0.8);
}
Apply polish to tables, KPI cards, and data-heavy interfaces. Data display is where monospace, tabular-nums, and clear visual hierarchy earn their keep. Every number should be scannable. Every column should align.
function KPICard({ label, value, trend, trendDirection }: Props) {
return (
<div className="elevation-card rounded-xl p-4 bg-card border border-border">
<p className="text-caption text-muted-foreground mb-1">{label}</p>
<div className="flex items-baseline gap-2">
<span className="text-display font-bold text-foreground tabular-nums">
{value}
</span>
{trend && (
<span
className={cn(
"text-caption font-medium flex items-center gap-0.5",
trendDirection === "up" ? "text-success" : "text-error",
)}
>
{trendDirection === "up" ? (
<TrendingUp className="h-3 w-3" />
) : (
<TrendingDown className="h-3 w-3" />
)}
{trend}
</span>
)}
</div>
</div>
);
}
// Table container
<div className="rounded-xl border border-border overflow-hidden">
<table className="w-full">
<thead>
<tr className="bg-muted/50 border-b border-border">
<th className="px-4 py-3 text-left text-label font-medium text-muted-foreground">
Column
</th>
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr
key={i}
className={cn(
"border-b border-border last:border-0",
"transition-colors duration-fast",
"hover:bg-muted/30",
)}
>
<td className="px-4 py-3 text-body">{row.value}</td>
</tr>
))}
</tbody>
</table>
</div>
// Numeric columns use tabular-nums
<td className="text-body tabular-nums text-right font-mono">
{formatNumber(value)}
</td>
For empty states in data views, use the generic EmptyState component from Phase 7.
Wrap charts in an elevation-card with a title row and fixed height. Use a consistent color palette for multi-series data:
const chartColors = [
"hsl(211, 100%, 50%)", // Blue (primary)
"hsl(160, 84%, 39%)", // Green
"hsl(38, 92%, 50%)", // Amber
"hsl(280, 65%, 60%)", // Purple
"hsl(350, 80%, 60%)", // Rose
];
.shimmer-effect {
position: relative;
overflow: hidden;
}
.shimmer-effect::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent,
var(--shimmer-highlight),
transparent
);
animation: shimmer 1.5s ease-in-out infinite;
}
:root {
--shimmer-highlight: rgba(255, 255, 255, 0.08);
}
.dark {
--shimmer-highlight: rgba(255, 255, 255, 0.04);
}
function Skeleton({ className }: { className?: string }) {
return (
<div className={cn("bg-muted/50 rounded-md shimmer-effect", className)} />
);
}
// Usage: <Skeleton className="h-4 w-32" /> (text), <Skeleton className="h-10 w-full" /> (input)
This is the integration phase. Every page should feel cohesive—the design system tokens, components, and patterns from Phases 1–8 should work together seamlessly. If something feels off, trace it back to the phase that owns it.
function PageHeader({ title, description }: Props) {
return (
<div className="mb-8">
<h1
className={cn(
"text-3xl font-bold tracking-tight",
"bg-clip-text text-transparent",
"bg-gradient-to-r from-foreground via-foreground/90 to-foreground/70",
)}
>
{title}
</h1>
{description && (
<p className="text-muted-foreground mt-2 text-base">{description}</p>
)}
</div>
);
}
Before shipping UI:
cursor: pointerprefers-reduced-motion)::selection)-webkit-backdrop-filter for Safari::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: hsl(var(--muted-foreground) / 0.3);
border-radius: 999px;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(var(--muted-foreground) / 0.5);
}
::selection {
background: hsl(var(--primary) / 0.3);
color: inherit;
}
@media (prefers-reduced-motion: reduce) {
.animate-fade-slide-up,
.animate-typing-dot,
.animate-chip-in,
.animate-pulse,
.stagger-item {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
/* Disable hover translations */
.hover\:-translate-y-px:hover,
.hover\:-translate-y-0\.5:hover,
.hover\:-translate-y-1:hover {
transform: none !important;
}
/* Keep transitions very short for focus states */
* {
transition-duration: 0.01ms !important;
}
}
@supports not (backdrop-filter: blur(24px)) {
.glass {
background: hsl(var(--card) / 0.95);
}
.glass-subtle {
background: hsl(var(--card) / 0.9);
}
.glass-strong {
background: hsl(var(--card));
}
}
.glass {
background: hsl(var(--card) / 0.6);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid hsl(var(--glass-border));
}
.glass-subtle {
background: hsl(var(--card) / 0.4);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid hsl(0 0% 100% / 0.04);
}
.glass-strong {
background: hsl(var(--card) / 0.8);
backdrop-filter: blur(32px);
-webkit-backdrop-filter: blur(32px);
border: 1px solid hsl(0 0% 100% / 0.08);
}
.glow-primary {
box-shadow:
0 0 20px hsl(var(--primary) / 0.15),
0 0 40px hsl(var(--primary) / 0.1);
}
.divider-glow {
height: 1px;
width: 100%;
background: linear-gradient(
to right,
transparent,
hsl(var(--primary) / 0.2),
transparent
);
}
box-shadow: 0 25px 50px...)/alpha syntax)<select> without custom stylingprefers-reduced-motion support-webkit-backdrop-filter for Safarioutline instead of ring for focus states| Pattern | Code |
| ------------------ | --------------------------------------------------------------------------------------------------- |
| Elevation-on-hover | hover:-translate-y-px active:translate-y-0 |
| Glass morphism | glass utility class (see Phase 9) |
| Transition stack | transition-all duration-250 ease-apple |
| Focus ring | focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2 |
| Gradient text | bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground/90 to-foreground/70 |
| Avatar gradient | bg-gradient-to-br from-primary to-primary/70 shadow-glow-blue |
| Tabular numbers | tabular-nums font-mono |
| Status colors | text-{status} bg-{status}/10 border-{status}/20 (success/warning/error/info) |
| Active glow | isActive && "shadow-glow-blue animate-pulse" |
.glass {
background: hsl(var(--card) / 0.6);
backdrop-filter: blur(24px);
border: 1px solid hsl(var(--glass-border));
box-shadow: var(--shadow-glass);
}
const statusColors = {
success: "text-success bg-success/10 border-success/20",
warning: "text-warning bg-warning/10 border-warning/20",
error: "text-error bg-error/10 border-error/20",
info: "text-info bg-info/10 border-info/20",
};
className={cn(
"transition-all duration-300",
isActive && "shadow-glow-blue animate-pulse",
isComplete && "shadow-card",
)}
// tailwind.config.js — structure overview (tokens defined in Phases 2-4)
module.exports = {
theme: {
extend: {
colors: {
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
primary: { DEFAULT: "hsl(var(--primary) / <alpha-value>)" },
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground) / <alpha-value>)",
},
card: { DEFAULT: "hsl(var(--card) / <alpha-value>)" },
border: { DEFAULT: "hsl(var(--border) / <alpha-value>)" },
"glass-border": "hsl(var(--glass-border))",
// ... success, warning, error, info semantic colors
},
boxShadow: {
ambient: "var(--shadow-ambient)",
card: "var(--shadow-card)",
"card-hover": "var(--shadow-card-hover)",
glass: "var(--shadow-glass)",
"glass-lg": "var(--shadow-glass-lg)",
elevated: "var(--shadow-elevated)",
"elevated-lg": "var(--shadow-elevated-lg)",
"glow-blue": "var(--shadow-glow-blue)",
"glow-blue-lg": "var(--shadow-glow-blue-lg)",
// ... glow-green, glow-red
},
transitionTimingFunction: {
apple: "cubic-bezier(0.25, 1, 0.5, 1)",
spring: "cubic-bezier(0.22, 1, 0.36, 1)",
},
transitionDuration: { 250: "250ms", 350: "350ms", 400: "400ms" },
keyframes: {
// ... animation keyframes from Phase 4 (fade-in, fade-slide-up, scale-in, modal-enter, shimmer)
},
borderRadius: {
xl: "var(--radius-xl)",
"2xl": "var(--radius-2xl)",
},
},
},
};
After implementing all phases, you should have:
src/
├── index.css # CSS variables, glass utilities, reduced motion
├── hooks/
│ └── use-animations.ts # useInView, useStagger, useCountUp, etc.
├── components/
│ ├── ui/
│ │ ├── Button.tsx # Glow effects, elevation
│ │ ├── Input.tsx # Glass borders, focus shift
│ │ ├── Select.tsx # Custom chevron
│ │ ├── Checkbox.tsx # Custom styled
│ │ ├── Modal.tsx # Entry/exit animations
│ │ └── Card.tsx # Variants: default, elevated, glass, interactive
│ └── layout/
│ ├── Sidebar.tsx # Color-coded icons, gradient logo
│ ├── Header.tsx # Glass effect
│ └── PageHeader.tsx # Gradient title
tailwind.config.js # Extended colors, shadows, animations
development
Systematic debugging with hypothesis-driven investigation. Use when something is broken, tests are failing, unexpected behavior occurs, or errors need investigation. Triggers on: 'this is broken', 'debug', 'why is this failing', 'unexpected error', 'not working', 'bug', 'fix this issue', 'investigate', 'tests failing', 'trace the error', 'use debug mode'. Full access mode - can run commands, add logging, and fix issues.
development
Systematic debugging with hypothesis-driven investigation. Use when something is broken, tests are failing, unexpected behavior occurs, or errors need investigation. Triggers on: 'this is broken', 'debug', 'why is this failing', 'unexpected error', 'not working', 'bug', 'fix this issue', 'investigate', 'tests failing', 'trace the error', 'use debug mode'. Full access mode - can run commands, add logging, and fix issues.
testing
Behavioral testing strategy — deciding what to test and how. Use when writing tests, reviewing test quality, or fixing tests that test mocks instead of behavior. Triggers on: 'use testing mode', 'write tests', 'test strategy', 'tests are brittle', 'tests test mocks', 'improve test quality', 'what should I test'. Full access mode - can write and run tests.
development
Use when finding code smells, auditing TODOs, removing dead code, cleaning up unused imports, or assessing code quality. Triggers on: 'use tech-debt mode', 'tech debt', 'code smells', 'clean up', 'remove dead code', 'delete unused', 'simplify'. Full access mode - can modify files and run tests.