ui-ux
UI/UX - Design Principles & Implementation
Create beautiful, functional interfaces that delight users
When to Use This Skill
Use this skill when:
- Designing new components or interfaces
- Implementing UI elements (buttons, forms, cards, modals)
- Reviewing visual design and user experience
- Suggesting design improvements or alternatives
- Validating component states and interactions
- Creating responsive layouts
- Implementing dark mode or themes
- Building design systems or component libraries
Don't use this skill when:
- Only implementing backend logic (no UI)
- Working with pure data processing
- Technical accessibility fixes (use accessibility skill for WCAG)
- Performance optimization without visual impact
Critical Patterns
Pattern 1: Visual Hierarchy
When: Organizing information and guiding user attention
Good:
// Clear hierarchy with size, weight, color
<div className="space-y-4">
{/* Primary: Large, bold, high contrast */}
<h1 className="text-4xl font-bold text-gray-900">
Create Your Account
</h1>
{/* Secondary: Medium, regular, medium contrast */}
<p className="text-lg text-gray-600">
Get started with a free 14-day trial
</p>
{/* Tertiary: Small, light, low contrast */}
<span className="text-sm text-gray-400">
No credit card required
</span>
</div>
Bad:
// ❌ No hierarchy - everything same size/weight
<div>
<h1 className="text-base">Create Your Account</h1>
<p className="text-base">Get started with a free 14-day trial</p>
<span className="text-base">No credit card required</span>
</div>
Why: Visual hierarchy guides user attention through size, weight, and contrast. Primary content should dominate, supporting content should recede.
Pattern 2: Comprehensive Component States
When: Implementing interactive components
Good:
const Button = ({ variant = 'primary', isLoading, disabled }) => {
return (
<button
disabled={disabled || isLoading}
className={cn(
// Base styles
"px-4 py-2 rounded-lg font-medium transition-all",
// Variant styles
variant === 'primary' && "bg-blue-600 text-white",
variant === 'secondary' && "bg-gray-200 text-gray-900",
// Interactive states
!disabled && !isLoading && "hover:opacity-90 active:scale-95",
// Focus state (keyboard navigation)
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2",
// Disabled state
disabled && "opacity-50 cursor-not-allowed",
// Loading state
isLoading && "cursor-wait"
)}
>
{isLoading && <Spinner className="mr-2" />}
{children}
</button>
);
};
Bad:
// ❌ Only handles default state
const Button = ({ children }) => {
return (
<button className="px-4 py-2 bg-blue-600 text-white rounded">
{children}
</button>
);
};
Why: Users need visual feedback for all interactions. Missing states create confusion and poor UX.
Required States:
- Default
- Hover (mouse users)
- Focus (keyboard users)
- Active/Pressed
- Disabled
- Loading
- Error (if applicable)
Pattern 3: Responsive Design with Mobile-First
When: Creating layouts that work across devices
Good:
// Mobile-first: start with mobile, enhance for desktop
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Cards stack on mobile, 2 cols on tablet, 3 on desktop */}
{products.map(product => (
<Card key={product.id}>
<img
src={product.image}
className="w-full h-48 object-cover"
alt={product.name}
/>
<div className="p-4">
<h3 className="text-lg font-semibold">{product.name}</h3>
<p className="text-sm text-gray-600 mt-2">{product.description}</p>
</div>
</Card>
))}
</div>
Bad:
// ❌ Desktop-only, breaks on mobile
<div className="grid grid-cols-3 gap-4">
{products.map(product => (
<Card>{/* Content */}</Card>
))}
</div>
Why: Mobile-first ensures core experience works everywhere, then progressively enhances for larger screens.
Touch Targets: Minimum 44x44px for mobile (48x48px recommended)
// ✅ Good touch target
<button className="min-h-[44px] min-w-[44px] p-3">
<Icon />
</button>
// ❌ Too small for touch
<button className="p-1">
<Icon />
</button>
Pattern 4: Dark Mode Implementation
When: Supporting light and dark themes
Good:
// Using CSS variables for theme switching
// globals.css
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
}
// Components use CSS variables
<div className="bg-background text-foreground">
<div className="bg-card text-card-foreground p-6 rounded-lg">
<button className="bg-primary text-primary-foreground">
Click me
</button>
</div>
</div>
Bad:
// ❌ Hardcoded colors, no dark mode support
<div className="bg-white text-gray-900">
<div className="bg-white text-black p-6 rounded-lg">
<button className="bg-blue-600 text-white">
Click me
</button>
</div>
</div>
Why: CSS variables allow instant theme switching. Hardcoded colors require conditional classes everywhere.
Dark Mode Considerations:
- Use semantic color names (
background,foreground, notwhite,black) - Reduce white (#FFF → #F9F9F9) to prevent eye strain
- Adjust shadows (use lighter shadows or borders in dark mode)
- Maintain contrast ratios (WCAG 4.5:1 minimum)
Pattern 5: Consistent Spacing System
When: Creating balanced, rhythmic layouts
Good:
// Use design tokens (spacing scale: 4px base)
const spacing = {
xs: '0.25rem', // 4px
sm: '0.5rem', // 8px
md: '1rem', // 16px
lg: '1.5rem', // 24px
xl: '2rem', // 32px
'2xl': '3rem', // 48px
};
<div className="space-y-8">
{/* Consistent 32px vertical rhythm */}
<section className="p-8">
<h2 className="mb-4">Section Title</h2>
<p className="mb-6">Description text</p>
<div className="grid gap-4">
{/* Cards with consistent 16px gap */}
</div>
</section>
</div>
Bad:
// ❌ Inconsistent, random spacing
<div style={{ marginBottom: '13px' }}>
<section style={{ padding: '17px' }}>
<h2 style={{ marginBottom: '9px' }}>Title</h2>
<p style={{ marginBottom: '21px' }}>Text</p>
</section>
</div>
Why: Consistent spacing creates visual rhythm and professional polish. Use multiples of 4px or 8px.
Pattern 6: Microinteractions for Feedback
When: Providing immediate user feedback
Good:
// Optimistic UI with animations
const LikeButton = ({ postId }) => {
const [isLiked, setIsLiked] = useState(false);
const [count, setCount] = useState(0);
const handleLike = async () => {
// Optimistic update
setIsLiked(!isLiked);
setCount(prev => isLiked ? prev - 1 : prev + 1);
try {
await likePost(postId);
} catch (error) {
// Rollback on error
setIsLiked(!isLiked);
setCount(prev => isLiked ? prev + 1 : prev - 1);
toast.error('Failed to like post');
}
};
return (
<button
onClick={handleLike}
className="group flex items-center gap-2 transition-all"
>
<Heart
className={cn(
"w-5 h-5 transition-all",
isLiked && "fill-red-500 text-red-500 scale-110",
!isLiked && "text-gray-400 group-hover:text-red-400"
)}
/>
<span className={cn(
"transition-all",
isLiked && "text-red-500 font-medium"
)}>
{count}
</span>
</button>
);
};
Bad:
// ❌ No feedback, waits for server response
const LikeButton = ({ postId }) => {
const [count, setCount] = useState(0);
const handleLike = async () => {
const result = await likePost(postId);
setCount(result.count);
};
return (
<button onClick={handleLike}>
Like {count}
</button>
);
};
Why: Immediate feedback feels responsive. Optimistic updates provide instant gratification while server processes.
Anti-Patterns
❌ Anti-Pattern 1: Inconsistent Component Variants
Don't do this:
// ❌ Inconsistent button styles across app
<button className="bg-blue-500 px-6 py-3 rounded">Save</button>
<button className="bg-blue-600 px-4 py-2 rounded-lg">Submit</button>
<button className="bg-indigo-500 px-5 py-2.5 rounded-md">Continue</button>
Why it's bad: Inconsistency looks unprofessional and confuses users.
Do this instead:
// ✅ Use a Button component with defined variants
<Button variant="primary">Save</Button>
<Button variant="primary">Submit</Button>
<Button variant="primary">Continue</Button>
// Button component enforces consistency
const Button = ({ variant = 'primary', size = 'md', children }) => {
const styles = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
};
const sizes = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
return (
<button className={cn(styles[variant], sizes[size], 'rounded-lg transition')}>
{children}
</button>
);
};
❌ Anti-Pattern 2: Poor Empty States
Don't do this:
// ❌ Just shows nothing
{products.length === 0 ? null : (
<ProductGrid products={products} />
)}
Why it's bad: Users don't know if it's loading, an error, or intentionally empty.
Do this instead:
// ✅ Helpful empty state with action
{products.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<ShoppingBag className="w-16 h-16 text-gray-300 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
No products yet
</h3>
<p className="text-gray-500 mb-6">
Get started by adding your first product
</p>
<Button onClick={() => router.push('/products/new')}>
Add Product
</Button>
</div>
) : (
<ProductGrid products={products} />
)}
❌ Anti-Pattern 3: Ignoring Loading States
Don't do this:
// ❌ No loading indicator
const Dashboard = () => {
const { data } = useQuery('stats');
return (
<div>
<h1>Dashboard</h1>
<Stats data={data} />
</div>
);
};
Why it's bad: User sees broken/empty UI while loading, or worse, errors.
Do this instead:
// ✅ Proper loading and error states
const Dashboard = () => {
const { data, isLoading, error } = useQuery('stats');
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Spinner className="w-8 h-8" />
</div>
);
}
if (error) {
return (
<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-red-800">Failed to load dashboard</p>
<Button onClick={() => refetch()} className="mt-2">
Try Again
</Button>
</div>
);
}
return (
<div>
<h1>Dashboard</h1>
<Stats data={data} />
</div>
);
};
❌ Anti-Pattern 4: Tiny Touch Targets on Mobile
Don't do this:
// ❌ 24px icon button - too small for fingers
<button className="p-1">
<X className="w-4 h-4" />
</button>
Why it's bad: Users will miss tap targets, causing frustration.
Do this instead:
// ✅ Minimum 44x44px touch target
<button className="p-3 min-w-[44px] min-h-[44px] flex items-center justify-center">
<X className="w-5 h-5" />
</button>
Rule: 44x44px minimum (iOS), 48x48px recommended (Material Design)
❌ Anti-Pattern 5: Poor Form Error Handling
Don't do this:
// ❌ Generic error, no field-specific feedback
<form onSubmit={handleSubmit}>
<input name="email" />
<input name="password" />
{error && <p>Error: {error}</p>}
<button type="submit">Submit</button>
</form>
Why it's bad: User doesn't know which field has the problem.
Do this instead:
// ✅ Field-specific errors with helpful messages
<form onSubmit={handleSubmit}>
<div className="space-y-1">
<input
name="email"
className={cn(
"border rounded-lg px-3 py-2",
errors.email && "border-red-500"
)}
/>
{errors.email && (
<p className="text-sm text-red-600 flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
{errors.email.message}
</p>
)}
</div>
<div className="space-y-1">
<input
name="password"
className={cn(
"border rounded-lg px-3 py-2",
errors.password && "border-red-500"
)}
/>
{errors.password && (
<p className="text-sm text-red-600 flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
{errors.password.message}
</p>
)}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
❌ Anti-Pattern 6: Overwhelming Modals
Don't do this:
// ❌ Modal with 20 fields, long form
<Dialog>
<DialogContent className="max-w-4xl">
<form>
{/* 20+ input fields */}
<input name="field1" />
<input name="field2" />
{/* ... 18 more fields */}
</form>
</DialogContent>
</Dialog>
Why it's bad: Cognitive overload, users abandon complex modals.
Do this instead:
// ✅ Multi-step wizard for complex forms
<Dialog>
<DialogContent>
<div className="mb-4">
<div className="flex gap-2">
<div className={cn("h-1 flex-1 rounded", step >= 1 && "bg-blue-600")} />
<div className={cn("h-1 flex-1 rounded", step >= 2 && "bg-blue-600")} />
<div className={cn("h-1 flex-1 rounded", step >= 3 && "bg-blue-600")} />
</div>
<p className="text-sm text-gray-500 mt-2">Step {step} of 3</p>
</div>
{step === 1 && <BasicInfoFields />}
{step === 2 && <ContactFields />}
{step === 3 && <PreferencesFields />}
<div className="flex justify-between mt-6">
{step > 1 && <Button onClick={prevStep}>Back</Button>}
<Button onClick={nextStep}>
{step === 3 ? 'Submit' : 'Continue'}
</Button>
</div>
</DialogContent>
</Dialog>
Code Examples
Example 1: Button Component with All States
export function Button({ variant = 'primary', isLoading, disabled, children }: ButtonProps) {
return (
<button
disabled={disabled || isLoading}
className={cn(
'px-4 py-2 rounded-lg font-medium transition-all',
variant === 'primary' && 'bg-blue-600 text-white',
variant === 'secondary' && 'bg-gray-200 text-gray-900',
!disabled && !isLoading && 'hover:opacity-90 active:scale-95',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
disabled && 'opacity-50 cursor-not-allowed',
isLoading && 'cursor-wait'
)}
>
{isLoading && <Spinner className="mr-2 w-4 h-4 animate-spin" />}
{children}
</button>
);
}
Example 2: Responsive Card Grid
export function ProductGrid({ products }: ProductGridProps) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{products.map(product => (
<div key={product.id} className="bg-white rounded-lg shadow-md overflow-hidden">
<img
src={product.image}
alt={product.name}
className="w-full h-48 object-cover"
/>
<div className="p-4 space-y-2">
<h3 className="text-lg font-semibold">{product.name}</h3>
<p className="text-sm text-gray-600">{product.description}</p>
<p className="text-xl font-bold text-blue-600">${product.price}</p>
</div>
</div>
))}
</div>
);
}
For comprehensive examples and detailed implementations, see the references/ folder.
Quick Reference
Visual Hierarchy Scale
| Element | Size | Weight | Color |
|---|---|---|---|
| Primary Heading | text-4xl (36px) | font-bold (700) | text-gray-900 |
| Secondary Heading | text-2xl (24px) | font-semibold (600) | text-gray-800 |
| Body Large | text-lg (18px) | font-normal (400) | text-gray-700 |
| Body | text-base (16px) | font-normal (400) | text-gray-600 |
| Small | text-sm (14px) | font-normal (400) | text-gray-500 |
| Caption | text-xs (12px) | font-normal (400) | text-gray-400 |
Component States Checklist
- Default
- Hover (
:hover) - Focus (
:focus-visible) - Active (
:active) - Disabled (
disabled,opacity-50) - Loading (
cursor-wait, spinner) - Error (
border-red-500, error message) - Success (if applicable)
Spacing Scale (Tailwind)
// 4px base scale
gap-1 = 4px
gap-2 = 8px
gap-4 = 16px
gap-6 = 24px
gap-8 = 32px
gap-12 = 48px
gap-16 = 64px
Responsive Breakpoints
sm: 640px // Small tablets
md: 768px // Tablets
lg: 1024px // Laptops
xl: 1280px // Desktops
2xl: 1536px // Large desktops
Color Contrast Ratios (WCAG)
| Level | Normal Text | Large Text |
|---|---|---|
| AA | 4.5:1 | 3:1 |
| AAA | 7:1 | 4.5:1 |
Resources
Design Systems:
- Tailwind CSS - Utility-first CSS framework
- shadcn/ui - Re-usable components built with Radix UI and Tailwind
- Radix UI - Unstyled, accessible components
- Material Design - Google's design system
Component Libraries:
- Headless UI - Unstyled, accessible components
- Ark UI - Headless UI for React, Vue, Solid
Design Tools:
- Figma - Collaborative design tool
- Contrast Checker - WCAG contrast validation
Related Skills:
- accessibility: WCAG compliance, ARIA, keyboard navigation
- react: Component implementation patterns
- radix-ui: Headless component primitives
- patterns: Design patterns and principles
References (Progressive Disclosure):
- Design Systems - Tokens, theming, component libraries
- Component Patterns - Buttons, forms, cards, modals
- Interaction Patterns - Navigation, feedback, animations
- Trending Patterns - Glassmorphism, neumorphism, modern UI trends
Maintained by dsmj-ai-toolkit. Inspired by modern design systems and user-centered design principles.