ui-design
Guides UI design decisions for polished, professional interfaces. Activates when creating components, layouts, cards, forms, tables, dashboards, or any user-facing UI. Covers visual hierarchy, spacing, typography, OKLCH color theming (light/dark mode), and Refactoring UI principles applied with Tailwind CSS v4 and shadcn/ui.
SKILL.md
| Name | ui-design |
| Description | Guides UI design decisions for polished, professional interfaces. Activates when creating components, layouts, cards, forms, tables, dashboards, or any user-facing UI. Covers visual hierarchy, spacing, typography, OKLCH color theming (light/dark mode), and Refactoring UI principles applied with Tailwind CSS v4 and shadcn/ui. |
name: ui-design description: "Guides UI design decisions for polished, professional interfaces. Activates when creating components, layouts, cards, forms, tables, dashboards, or any user-facing UI. Covers visual hierarchy, spacing, typography, OKLCH color theming (light/dark mode), and Refactoring UI principles applied with Tailwind CSS v4 and shadcn/ui."
UI Design Principles
This skill codifies the design principles from Refactoring UI (Adam Wathan & Steve Schoger) applied to this project's stack: Tailwind CSS v4 + shadcn/ui. Follow these rules to produce polished, professional interfaces without needing a designer.
IMPORTANT: These are not suggestions — they are rules. Every component you create should follow them. When in doubt, apply the "squint test": blur your eyes and check if the visual hierarchy still reads correctly.
MANDATORY: Always use shadcn/ui components when one exists for the element you need. Never use raw HTML elements (<input>, <select>, <textarea>, <label>, <table>) when a shadcn/ui primitive is available. shadcn/ui components already apply the correct theme tokens, focus rings, border radii, and accessibility attributes. If a component isn't installed yet, install it with npx shadcn@latest add [component-name].
shadcn/ui Component Map
| Need | Use | NOT |
|---|---|---|
| Text input | <Input> from @/components/ui/input | <input> |
| Textarea | <Textarea> from @/components/ui/textarea | <textarea> |
| Select dropdown | <Select> from @/components/ui/select | <select> |
| Native select | <NativeSelect> from @/components/ui/native-select | <select> |
| Label | <Label> from @/components/ui/label | <label> |
| Button | <Button> from @/components/ui/button | <button> |
| Card container | <Card>, <CardHeader>, <CardContent> | <div> with card classes |
| Data table | <Table>, <TableHeader>, <TableRow>, <TableCell> | <table>, <thead>, <tr>, <td> |
| Badge/status | <Badge> from @/components/ui/badge | <span> with pill classes |
| Checkbox | <Checkbox> from @/components/ui/checkbox | <input type="checkbox"> |
| Switch/toggle | <Switch> from @/components/ui/switch | <input type="checkbox"> |
| Dialog/modal | <Dialog> from @/components/ui/dialog | custom modal <div> |
| Separator | <Separator> from @/components/ui/separator | <hr> or border-b dividers |
1. Visual Hierarchy
Visual hierarchy is the most important design concept. It determines how users scan and understand a page.
Rules
- Three levers: size, weight/boldness, and color/contrast. Combine them — don't multiply. A heading should be large OR bold OR dark, rarely all three.
- Primary content: larger, bolder, darker (e.g.,
text-lg font-semibold text-foreground). - Secondary content: smaller, normal weight, muted (e.g.,
text-sm text-muted-foreground). - Tertiary content: smallest, lightest (e.g.,
text-xs text-muted-foreground/70). - Emphasize by de-emphasizing: instead of making the primary element louder, make surrounding elements quieter.
- Labels are secondary: form labels, table headers, and metadata labels support data — they shouldn't compete. Use
text-sm font-medium text-muted-foregroundfor labels, not bold large text. - Skip labels when the format is obvious: email addresses, phone numbers, dates, and URLs don't need labels — their format is self-explanatory.
Tailwind Patterns
{/* ✅ Clear hierarchy (theme-aware) */}
<div>
<h2 className="text-lg font-semibold text-foreground">Project Alpha</h2>
<p className="text-sm text-muted-foreground">Created 3 days ago</p>
</div>
{/* ❌ No hierarchy — everything looks the same */}
<div>
<h2 className="text-base font-normal text-foreground">Project Alpha</h2>
<p className="text-base font-normal text-foreground">Created 3 days ago</p>
</div>
2. Spacing & Layout
Spacing Scale
Use Tailwind's constrained scale. Don't use arbitrary values.
| Tailwind | px | Use for |
|---|---|---|
gap-1 / p-1 | 4px | Icon + label gap, tight coupling |
gap-2 / p-2 | 8px | Related items within a group |
gap-3 / p-3 | 12px | List items, small card padding |
gap-4 / p-4 | 16px | Standard content padding, form fields |
gap-6 / p-6 | 24px | Card padding, section gaps |
gap-8 / p-8 | 32px | Major section separation |
gap-12 / p-12 | 48px | Page section spacing |
gap-16 / p-16 | 64px | Hero spacing, page top padding |
Rules
- Start with more space, then reduce: add more padding/margin than you think you need. It's easier to reduce than to add.
- Group spacing rule: space BETWEEN groups must be larger than space WITHIN groups. This creates visual grouping without borders.
- Don't stretch to fill: if content only needs 600px, use
max-w-2xl— don't spread it across the full viewport. - Mobile spacing: reduce spacing on small screens. What's
p-8on desktop might bep-4on mobile. - Consistent scale: don't mix arbitrary values. Stick to Tailwind's spacing scale.
Tailwind Patterns
{/* ✅ Proper grouping — more space between groups than within */}
<div className="space-y-8"> {/* 32px between sections */}
<section className="space-y-3"> {/* 12px within section */}
<h3 className="text-lg font-semibold">Section Title</h3>
<p className="text-sm text-gray-500">Description text</p>
</section>
<section className="space-y-3">
<h3 className="text-lg font-semibold">Another Section</h3>
<p className="text-sm text-gray-500">More description</p>
</section>
</div>
{/* ✅ Constrained width */}
<div className="mx-auto max-w-2xl">
{/* Content that doesn't need full width */}
</div>
3. Typography
Rules
- Use neutral sans-serifs for UI: the project uses Geist (loaded locally). It's a well-crafted neutral font — don't add more fonts.
- Line length: 45–75 characters per line for readability. Use
max-w-prose(~65ch) ormax-w-2xl. - Left-align text longer than 2-3 lines. Center-align only for short headings or CTAs.
- Right-align numbers in tables and data displays for easy scanning.
- Font weight minimum: never go below
font-normal(400) for UI text. To de-emphasize, use smaller size or lighter color instead. - Non-linear font scale: use Tailwind's built-in scale (
text-xsthroughtext-4xl). Don't create custom sizes between adjacent steps. - Letter spacing: add
tracking-wideto uppercase text or small labels. Don't add letter-spacing to body text.
Tailwind Patterns
{/* ✅ Readable paragraph (theme-aware) */}
<p className="max-w-prose text-base leading-relaxed text-muted-foreground">
Long paragraph text here...
</p>
{/* ✅ Right-aligned numbers in table */}
<td className="text-right tabular-nums text-sm text-foreground">1,234.56</td>
{/* ✅ Uppercase small label */}
<span className="text-xs font-medium uppercase tracking-wide text-muted-foreground">Status</span>
4. Color & Theming
Color Theory Foundations
The project uses OKLCH (Oklab Lightness, Chroma, Hue) — a perceptually uniform color space where equal numeric changes produce visually equal changes. This makes palette generation predictable: adjusting lightness doesn't shift hue, and colors at the same lightness appear equally bright.
OKLCH(Lightness, Chroma, Hue)
Lightness: 0 (black) → 1 (white)
Chroma: 0 (gray) → 0.4 (vivid)
Hue: 0–360° (color wheel angle)
Color Harmony Schemes (from the color wheel):
- Monochromatic: one hue, vary lightness/chroma. Used for our neutral base.
- Complementary: opposite hues (180° apart). Maximum contrast — use sparingly.
- Analogous: adjacent hues (±30°). Harmonious — good for related UI elements.
- Triadic: three hues 120° apart. Vibrant — good for chart palettes.
- Split-complementary: one hue + two adjacent to its complement. Balanced contrast.
The 60-30-10 Rule
Applied to the semantic token system in globals.css:
| Proportion | Tokens | Purpose |
|---|---|---|
| 60% | background, card, popover | Neutral surfaces — the canvas |
| 30% | secondary, muted, accent | Supporting tones — subtle differentiation |
| 10% | primary, destructive | Chromatic attention — actions and alerts |
Theme Token System
All colors are defined as CSS custom properties in globals.css with :root (light) and .dark (dark) overrides. The @theme inline directive maps them to Tailwind utility classes.
Token naming convention — every token follows the background/foreground pair pattern:
--primary → background color of primary elements
--primary-foreground → text color ON primary backgrounds
Always pair them: bg-primary text-primary-foreground. This ensures contrast in both modes.
Complete token map:
| Token | Light Mode Purpose | Dark Mode Behavior |
|---|---|---|
background / foreground | Page background + body text | Inverted lightness |
card / card-foreground | Card surface + text | Slightly elevated from background |
popover / popover-foreground | Dropdown/dialog surface | Same as card in dark |
primary / primary-foreground | Brand action color + its text | Inverted: light on dark surface |
secondary / secondary-foreground | Subtle background + text | Darker neutral |
muted / muted-foreground | Disabled/subdued areas + text | Darker neutral |
accent / accent-foreground | Hover highlights + text | Darker neutral |
destructive / destructive-foreground | Error/danger elements | Higher lightness for dark bg |
border | Borders and dividers | Semi-transparent white |
input | Input field borders | Slightly brighter than border |
ring | Focus rings | Adjusted for visibility |
Rules
- Design in grayscale first: get hierarchy right with spacing, size, and weight before adding color.
- Use semantic tokens, not raw colors:
bg-primarynotbg-blue-600,text-muted-foregroundnottext-gray-500. Tokens adapt to light/dark mode automatically. - Use dark grays, not black:
text-foreground(oklch 0.145) instead oftext-black. Pure black feels harsh. - Don't use gray text on colored backgrounds: use white with reduced opacity (
text-primary-foreground/70) or a color tinted toward the background. - Primary colors for actions: buttons, links, active states. Use
bg-primary text-primary-foreground. - Accent borders:
border-t-4 border-t-primaryfor visual flair on cards. - Semantic colors:
destructivefor errors/danger. For success/warning/info, define custom token pairs if needed (seeexamples/custom-theme-color.css).
Branding the Theme
To apply a brand color, change --primary in globals.css to the brand's OKLCH value:
/* Blue brand — hue 260° */
:root {
--primary: oklch(0.488 0.243 264);
--primary-foreground: oklch(0.985 0 0);
}
.dark {
--primary: oklch(0.688 0.2 264);
--primary-foreground: oklch(0.145 0 0);
}
To derive related colors from a brand hue, use color harmony:
- Accent: shift hue ±30° (analogous) — harmonious complement
- Chart palette: distribute across hue wheel at 72° intervals (360° / 5 colors)
- Destructive: keep at red hue (~27°) — it's semantic, not brand
Tailwind Patterns
{/* ✅ Semantic tokens — adapts to light/dark mode automatically */}
<h1 className="text-foreground">Title</h1>
<p className="text-muted-foreground">Secondary text</p>
{/* ✅ Card using theme tokens */}
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
<h3 className="text-card-foreground">Card Title</h3>
</div>
{/* ✅ Accent border using theme token */}
<div className="rounded-lg border border-border border-t-4 border-t-primary bg-card p-6 shadow-sm">
Card content
</div>
{/* ✅ On colored background — use foreground pair with opacity */}
<div className="bg-primary p-4">
<h3 className="font-semibold text-primary-foreground">Title</h3>
<p className="text-primary-foreground/70">Secondary text</p>
</div>
5. Shadows & Depth
Rules
- Use shadows instead of borders: shadows create softer separation and feel more polished. Reserve borders for dividers within a container.
- Shadow scale matches elevation:
shadow-smfor subtle lift (cards at rest),shadow-mdfor moderate (dropdowns, popovers),shadow-lgfor high (modals, dialogs). - Combine shadow + border for cards:
shadow-sm border border-bordercreates a subtle, polished card. - Separation without borders: use background color changes, extra spacing, or shadows to create visual separation between sections.
Tailwind Patterns
{/* ✅ Card with shadow + subtle border (theme-aware) */}
<div className="rounded-lg border border-border bg-card p-6 shadow-sm">
Card content
</div>
{/* ✅ Separation via background color (no border needed) */}
<div className="bg-muted p-8">
<div className="bg-card p-6 rounded-lg shadow-sm">
Card in section
</div>
</div>
{/* ❌ Too many borders — feels heavy */}
<div className="border border-border p-4">
<div className="border-b border-border pb-2 mb-2">Header</div>
<div className="border-b border-border pb-2 mb-2">Item 1</div>
<div>Item 2</div>
</div>
6. Actions & Buttons
Rules
- Hierarchy-first: not every action needs to be a prominent button. Primary actions get filled buttons, secondary get outlined/ghost, tertiary get text links.
- Destructive ≠ prominent: a delete button in a settings page doesn't need to be big and red. Use a ghost button or link style with destructive color.
- One primary action per section: if everything is primary, nothing is primary.
- Button sizing: use shadcn/ui variants (
default,outline,ghost,destructive,link) and sizes (sm,default,lg).
Tailwind Patterns
import { Button } from "@/components/ui/button";
{/* ✅ Action hierarchy */}
<div className="flex items-center gap-3">
<Button>Save Changes</Button> {/* Primary */}
<Button variant="outline">Cancel</Button> {/* Secondary */}
<Button variant="ghost" size="sm">Reset</Button> {/* Tertiary */}
</div>
{/* ✅ Destructive but not screaming */}
<Button variant="ghost" className="text-destructive hover:text-destructive">
Delete Account
</Button>
7. Forms
Rules
- Use shadcn/ui form primitives:
<Input>,<Textarea>,<Select>,<Label>,<Checkbox>,<Switch>. Never use raw HTML form elements. - Labels above inputs, not beside (better for mobile and scanning).
- Don't use placeholder as label: placeholders disappear on focus, losing context.
- Group related fields: use
space-y-4within a group,space-y-8between groups. - Error messages below the field: use
text-sm text-destructive. - Optional fields should say "(optional)" — don't mark required fields with asterisks.
- Input widths should match expected content: an email field can be full width, but a zip code field should be narrow.
- Install missing components: if
<Input>or<Label>aren't installed, runnpx shadcn@latest add input label. - All forms use
react-hook-formwithzodResolver(schema)— Zod schemas reused as validators - One
[Entity]Formcomponent per feature, reused for create and edit defaultValuesalways explicit. Mutations handled by parent, not the form
Tailwind Patterns
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
{/* ✅ Form field with shadcn/ui components */}
<div className="space-y-1.5">
<Label htmlFor="email">Email address</Label>
<Input id="email" type="email" />
</div>
{/* ✅ Select with shadcn/ui */}
<div className="space-y-1.5">
<Label htmlFor="role">Role</Label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
</SelectContent>
</Select>
</div>
{/* ✅ Optional field */}
<div className="space-y-1.5">
<Label htmlFor="bio">
Bio <span className="font-normal text-muted-foreground">(optional)</span>
</Label>
<Textarea id="bio" />
</div>
{/* ✅ Form with grouped sections */}
<form className="space-y-8">
<fieldset className="space-y-4">
<legend className="text-base font-semibold text-foreground">Personal Info</legend>
{/* Label + Input fields */}
</fieldset>
<fieldset className="space-y-4">
<legend className="text-base font-semibold text-foreground">Address</legend>
{/* Label + Input fields */}
</fieldset>
</form>
8. Cards & Lists
Rules
- Use
<Card>from shadcn/ui: use<Card>,<CardHeader>,<CardTitle>,<CardDescription>,<CardContent>,<CardFooter>— not raw<div>with card classes. Install withnpx shadcn@latest add card. - Use
<Badge>for status indicators: install withnpx shadcn@latest add badge. Don't hand-craft pill<span>elements. - Card content hierarchy: title (semibold,
text-card-foreground) → description (text-muted-foreground) → metadata (small, lightest). - List item spacing:
divide-y divide-borderfor bordered lists,space-y-2for spaced lists. - Empty states: always show
EmptyStatefrom@/features/feedback— never a blank space.
Tailwind Patterns
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
{/* ✅ Card with shadcn/ui components */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-sm">Card Title</CardTitle>
<Badge variant="secondary">Active</Badge>
</div>
<CardDescription>Description goes here</CardDescription>
</CardHeader>
<CardFooter className="text-xs text-muted-foreground/70">
<span>Updated 2h ago</span>
<span className="ml-4">3 tasks</span>
</CardFooter>
</Card>
9. Tables & Data
Rules
- Use
<Table>from shadcn/ui: use<Table>,<TableHeader>,<TableBody>,<TableRow>,<TableHead>,<TableCell>— not raw HTML table elements. Install withnpx shadcn@latest add table. - Right-align numbers, left-align text.
- De-emphasize headers: table headers are labels —
<TableHead>already applies muted styling. - Alternate row shading or use subtle borders — not both.
<TableRow>supports hover states. - Constrain column widths for long text: use
truncateormax-w-xsto prevent layout blow-up.
10. Responsive Design
Rules
- Mobile-first: base classes are for mobile,
md:andlg:for larger screens. - Reduce spacing on mobile:
p-4 md:p-6 lg:p-8. - Reduce heading scale on mobile: desktop heading may be
text-3xl, mobile may betext-xl. - Stack on mobile, side-by-side on desktop:
flex flex-col md:flex-row. - Don't shrink everything proportionally: some elements (icons, buttons) have minimum usable sizes.
Design Checklist
Before finishing any UI component, verify:
- Squint test: blur your eyes — does the hierarchy still read? Primary content stands out?
- Grayscale test: remove color mentally — does hierarchy work without it?
- Theme test: toggle light/dark mode — do all elements remain readable?
- Semantic tokens: using
bg-card,text-foreground,border-border— not hardcoded colors? - Token pairing: every
bg-[token]has its matchingtext-[token]-foreground? - Spacing: consistent scale, more space between groups than within
- Typography: clear size/weight hierarchy, readable line length (max-w-prose)
- Labels: de-emphasized, skipped when format is obvious
- Empty state: shows
EmptyState, not blank space ornull - Actions: clear primary/secondary/tertiary hierarchy
- Responsive: tested at 375px and 1280px
- Shadows: used instead of heavy borders where possible
- shadcn/ui: using
<Input>,<Label>,<Select>,<Card>,<Table>,<Badge>— not raw HTML?
DO NOT
- DO NOT use
text-black— usetext-foregroundfor the darkest text. - DO NOT use hardcoded colors (
bg-white,text-gray-500,border-gray-200) — use semantic tokens (bg-card,text-muted-foreground,border-border) so themes work. - DO NOT use
bg-[color]without its pairedtext-[color]-foreground— contrast breaks in dark mode. - DO NOT give every piece of content equal visual weight — establish hierarchy.
- DO NOT use borders for every separation — prefer shadows, spacing, or background colors.
- DO NOT use gray text on colored backgrounds — use the foreground pair with opacity.
- DO NOT make all buttons look equally important — one primary, rest secondary/ghost.
- DO NOT skip empty states — use
EmptyStatefrom@/features/feedback. - DO NOT use arbitrary spacing values — stick to Tailwind's constrained scale.
- DO NOT center-align text longer than 2-3 lines — left-align for readability.
- DO NOT go below
font-normal(400) weight — de-emphasize with size or color instead. - DO NOT make destructive actions visually dominant by default — most are secondary actions.
- DO NOT define new colors without both
:rootand.darkvalues — themes will break. - DO NOT use raw HTML form elements (
<input>,<select>,<textarea>,<label>) — always use shadcn/ui components (<Input>,<Select>,<Textarea>,<Label>). - DO NOT use raw
<table>elements — use shadcn/ui<Table>components. - DO NOT hand-craft card or badge markup — use shadcn/ui
<Card>and<Badge>components.