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 , not white , 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.