UX and UI Design
Use this skill for interface structure, user flows, visual hierarchy, and interaction design. Good UX/UI makes features intuitive and delightful to use.
Table of Contents
- •Design Principles
- •Layout Patterns
- •Visual Hierarchy
- •Interaction Design
- •Responsive Design
- •Common Patterns
- •Anti-Patterns to Avoid
Design Principles
1. Clarity Over Cleverness
Users should immediately understand what they can do and how to do it.
typescript
// ❌ UNCLEAR: What does this do? <button className="p-4 rounded-full bg-gradient-to-r from-purple-500 to-pink-500"> <SparklesIcon /> </button> // ✅ CLEAR: Action is obvious <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"> Create New Post <PlusIcon className="ml-2 w-4 h-4" aria-hidden="true" /> </button>
2. Consistency
Use the same patterns throughout the application.
| Element | Pattern | Example |
|---|---|---|
| Primary actions | Blue filled button | "Save Changes" |
| Secondary actions | Gray outline button | "Cancel" |
| Destructive actions | Red filled button | "Delete Account" |
| Danger warnings | Red background + icon | ⚠️ "This action cannot be undone" |
| Success messages | Green background + icon | ✅ "Profile saved!" |
3. Progressive Disclosure
Show only what's needed, when it's needed.
typescript
// ❌ OVERWHELMING: All options at once
<form>
<input placeholder="Name" />
<input placeholder="Email" />
<input placeholder="Phone" />
<input placeholder="Address" />
<input placeholder="City" />
<input placeholder="State" />
<input placeholder="Zip" />
<select>{/* 50 country options */}</select>
<textarea placeholder="Bio (optional, max 500 chars)" rows={10} />
{/* 20 more fields... */}
<button>Submit</button>
</form>
// ✅ PROGRESSIVE: Multi-step form
<FormStep current={1} total={3}>
<h2>Let's start with your basic info</h2>
<input placeholder="Name" />
<input placeholder="Email" />
<button>Next Step</button>
</FormStep>
4. Feedback & Response
Always acknowledge user actions immediately.
typescript
// ❌ NO FEEDBACK: User wonders if click worked
<button onClick={saveProfile}>
Save Profile
</button>
// ✅ IMMEDIATE FEEDBACK: User knows action is processing
<button
onClick={saveProfile}
disabled={isSaving}
className={isSaving ? 'opacity-50 cursor-not-allowed' : ''}
>
{isSaving ? (
<>
<Spinner className="mr-2" />
Saving...
</>
) : (
'Save Profile'
)}
</button>
Layout Patterns
Container Widths
typescript
// Standard responsive container
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Content */}
</div>
// Reading-optimized (narrower for text)
<article className="max-w-2xl mx-auto px-4">
{/* Long-form content */}
</article>
// Full-width (dashboards, tables)
<div className="w-full p-4">
{/* Wide content */}
</div>
Grid Layouts
typescript
// Responsive grid (1 col mobile, 2 col tablet, 3 col desktop)
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{items.map(item => (
<Card key={item.id} {...item} />
))}
</div>
// Auto-fit grid (fills available space)
<div className="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
{items.map(item => (
<Card key={item.id} {...item} />
))}
</div>
Sidebar Layouts
typescript
// Responsive sidebar (stacks on mobile)
<div className="flex flex-col lg:flex-row gap-6">
{/* Sidebar */}
<aside className="w-full lg:w-64 flex-shrink-0">
<FilterPanel />
</aside>
{/* Main content */}
<main className="flex-1 min-w-0">
<ResultsList />
</main>
</div>
Visual Hierarchy
Typography Scale
typescript
// Heading hierarchy <h1 className="text-4xl font-bold">Page Title</h1> <h2 className="text-3xl font-bold mt-8">Section Heading</h2> <h3 className="text-2xl font-semibold mt-6">Subsection</h3> <h4 className="text-xl font-medium mt-4">Card Heading</h4> // Body text <p className="text-base leading-7"> Standard paragraph text with comfortable line height (175%). </p> // Small text (metadata, captions) <span className="text-sm text-gray-600"> Posted 2 hours ago </span>
Spacing System
typescript
// Consistent spacing scale (4px base unit)
const spacing = {
xs: '0.25rem', // 4px - Tight spacing
sm: '0.5rem', // 8px - Small gaps
md: '1rem', // 16px - Default spacing
lg: '1.5rem', // 24px - Section spacing
xl: '2rem', // 32px - Large gaps
'2xl': '3rem', // 48px - Major sections
};
// Usage
<div className="space-y-4"> {/* 16px vertical gaps */}
<Card />
<Card />
<Card />
</div>
<section className="mb-12"> {/* 48px margin bottom */}
{/* Section content */}
</section>
Color Hierarchy
typescript
// Text hierarchy <div> <h2 className="text-gray-900">Primary Heading</h2> <p className="text-gray-700">Body text</p> <span className="text-gray-500">Secondary text</span> <small className="text-gray-400">Tertiary text</small> </div> // Interactive elements <button className="bg-blue-500 hover:bg-blue-600 active:bg-blue-700"> Primary Action </button> <button className="bg-gray-200 hover:bg-gray-300 active:bg-gray-400"> Secondary Action </button>
Interaction Design
Button States
typescript
interface ButtonProps {
variant: 'primary' | 'secondary' | 'destructive';
isLoading?: boolean;
disabled?: boolean;
}
export default function Button({ variant, isLoading, disabled, children }: ButtonProps) {
const baseStyles = 'px-4 py-2 rounded-lg font-medium transition-colors focus:ring-2 focus:ring-offset-2';
const variantStyles = {
primary: 'bg-blue-500 text-white hover:bg-blue-600 focus:ring-blue-500 disabled:bg-blue-300',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500 disabled:bg-gray-100',
destructive: 'bg-red-500 text-white hover:bg-red-600 focus:ring-red-500 disabled:bg-red-300',
};
return (
<button
className={`${baseStyles} ${variantStyles[variant]}`}
disabled={disabled || isLoading}
>
{isLoading ? (
<>
<Spinner className="mr-2 w-4 h-4 animate-spin" />
Loading...
</>
) : (
children
)}
</button>
);
}
Form Input States
typescript
// Visual states for form inputs
<input
type="text"
className={cn(
'w-full px-4 py-2 rounded-lg border transition-colors',
{
// Default
'border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-500': !error,
// Error
'border-red-500 focus:border-red-500 focus:ring-2 focus:ring-red-500': error,
// Disabled
'bg-gray-100 cursor-not-allowed': disabled,
}
)}
disabled={disabled}
aria-invalid={error ? 'true' : 'false'}
aria-describedby={error ? 'error-message' : undefined}
/>
{error && (
<p id="error-message" className="text-sm text-red-600 mt-1" role="alert">
{error}
</p>
)}
Loading Patterns
typescript
// Skeleton loading (best for known layouts)
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 rounded w-full"></div>
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
</div>
// Spinner (unknown content)
<div className="flex items-center justify-center p-8">
<Spinner className="w-8 h-8 animate-spin text-blue-500" />
<span className="ml-3 text-gray-600">Loading...</span>
</div>
// Progressive loading (show partial content)
<div>
{partialData.length > 0 && (
<div className="opacity-50">
{partialData.map(item => <Item key={item.id} {...item} />)}
</div>
)}
{isLoading && <Spinner />}
</div>
Responsive Design
Mobile-First Approach
typescript
// ❌ DESKTOP-FIRST: Hard to simplify for mobile
<nav className="flex gap-8 text-lg px-12">
<Link>Dashboard</Link>
<Link>Projects</Link>
<Link>Team</Link>
<Link>Settings</Link>
<Link>Analytics</Link>
<Link>Reports</Link>
</nav>
// ✅ MOBILE-FIRST: Start simple, enhance for desktop
<nav className="relative">
{/* Mobile: Hamburger menu */}
<button
className="lg:hidden p-2"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
>
<MenuIcon />
</button>
{/* Mobile: Drawer */}
{menuOpen && (
<div className="absolute top-full left-0 w-full bg-white shadow-lg lg:hidden">
<Link className="block px-4 py-3 hover:bg-gray-50">Dashboard</Link>
<Link className="block px-4 py-3 hover:bg-gray-50">Projects</Link>
{/* ... */}
</div>
)}
{/* Desktop: Horizontal */}
<div className="hidden lg:flex gap-6">
<Link>Dashboard</Link>
<Link>Projects</Link>
{/* ... */}
</div>
</nav>
Breakpoint Guidelines
typescript
// Tailwind breakpoints
const breakpoints = {
sm: '640px', // Large phones
md: '768px', // Tablets
lg: '1024px', // Small desktops
xl: '1280px', // Large desktops
'2xl': '1536px', // Extra large
};
// Usage
<div className="
p-4 {/* Mobile: 16px padding */}
md:p-6 {/* Tablet: 24px padding */}
lg:p-8 {/* Desktop: 32px padding */}
">
<h1 className="
text-2xl {/* Mobile: 24px */}
md:text-3xl {/* Tablet: 30px */}
lg:text-4xl {/* Desktop: 36px */}
">
Responsive Heading
</h1>
</div>
Common Patterns
Card Design
typescript
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow">
{/* Image */}
<img
src={post.image}
alt={post.title}
className="w-full h-48 object-cover"
/>
{/* Content */}
<div className="p-6">
{/* Category badge */}
<span className="inline-block px-3 py-1 text-xs font-medium bg-blue-100 text-blue-800 rounded-full mb-3">
{post.category}
</span>
{/* Title */}
<h3 className="text-xl font-bold mb-2">
{post.title}
</h3>
{/* Excerpt */}
<p className="text-gray-600 mb-4 line-clamp-3">
{post.excerpt}
</p>
{/* Metadata */}
<div className="flex items-center text-sm text-gray-500">
<Avatar src={post.author.avatar} />
<span className="ml-2">{post.author.name}</span>
<span className="mx-2">·</span>
<time dateTime={post.publishedAt}>
{formatDate(post.publishedAt)}
</time>
</div>
</div>
</div>
Modal Dialog
typescript
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
{/* Backdrop */}
<div className="fixed inset-0 bg-black/50" aria-hidden="true" />
{/* Modal container */}
<div className="fixed inset-0 flex items-center justify-center p-4">
{/* Modal panel */}
<Dialog.Panel className="bg-white rounded-lg max-w-md w-full p-6 shadow-xl">
{/* Header */}
<div className="flex items-start justify-between mb-4">
<Dialog.Title className="text-xl font-bold">
{title}
</Dialog.Title>
<button
onClick={() => setIsOpen(false)}
className="text-gray-400 hover:text-gray-600"
aria-label="Close dialog"
>
<XIcon className="w-6 h-6" />
</button>
</div>
{/* Content */}
<Dialog.Description className="text-gray-600 mb-6">
{description}
</Dialog.Description>
{/* Actions */}
<div className="flex gap-3 justify-end">
<button
onClick={() => setIsOpen(false)}
className="px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Cancel
</button>
<button
onClick={handleConfirm}
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600"
>
Confirm
</button>
</div>
</Dialog.Panel>
</div>
</Dialog>
Data Table
typescript
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{items.map(item => (
<tr key={item.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap font-medium">
{item.name}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={cn(
'px-2 py-1 text-xs font-medium rounded-full',
item.status === 'active' && 'bg-green-100 text-green-800',
item.status === 'pending' && 'bg-yellow-100 text-yellow-800',
item.status === 'inactive' && 'bg-gray-100 text-gray-800',
)}>
{item.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right">
<button className="text-blue-600 hover:text-blue-900">
Edit
</button>
<button className="ml-4 text-red-600 hover:text-red-900">
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
Anti-Patterns to Avoid
❌ Low Contrast
typescript
// ❌ BAD: Light gray text on white (fails WCAG AA) <p className="text-gray-300"> This text is hard to read </p> // ✅ GOOD: Sufficient contrast (>4.5:1) <p className="text-gray-700"> This text is easy to read </p>
❌ Tiny Touch Targets
typescript
// ❌ BAD: Too small for mobile (< 44px) <button className="p-1 text-xs"> <TrashIcon className="w-3 h-3" /> </button> // ✅ GOOD: Minimum 44x44px touch target <button className="p-3"> <TrashIcon className="w-5 h-5" aria-label="Delete" /> </button>
❌ No Visual Feedback
typescript
// ❌ BAD: No hover state, no disabled state
<button onClick={save}>
Save
</button>
// ✅ GOOD: Clear states
<button
onClick={save}
disabled={isDisabled}
className="
bg-blue-500 text-white
hover:bg-blue-600
active:scale-95
disabled:opacity-50 disabled:cursor-not-allowed
transition-all
"
>
Save
</button>
❌ Inconsistent Patterns
typescript
// ❌ BAD: Different patterns for the same action <button className="bg-blue-500 text-white">Create Post</button> <button className="border border-green-500 text-green-500">Add Comment</button> <a href="/new" className="text-purple-600 underline">New Project</a> // ✅ GOOD: Consistent primary action pattern <button className="bg-blue-500 text-white">Create Post</button> <button className="bg-blue-500 text-white">Add Comment</button> <button className="bg-blue-500 text-white">New Project</button>
Design Checklist
Before shipping a UI feature:
- • Visual hierarchy is clear (most important element stands out)
- • Color contrast meets WCAG AA (4.5:1 for text)
- • Touch targets are at least 44x44px on mobile
- • Loading states are shown for async operations
- • Error states are user-friendly and actionable
- • Empty states guide users to next action
- • Focus indicators are visible (2px ring)
- • Hover states provide feedback
- • Disabled states are visually distinct
- • Mobile layout works down to 320px width
- • Typography scale is consistent
- • Spacing follows design system
- • Interactions feel responsive (< 100ms feedback)
References
- •Component Library - Reusable components
- •Best Practices - Code standards
- •Accessibility Skill - A11y patterns
- •Content Strategy Skill - UI copy
- •Tailwind CSS Documentation - Utility classes
Skill Status: Enhanced Last Updated: 2026-02-13 (Phase 5)