| name | frontend-design |
| description | Use when designing UI components, implementing accessibility, responsive layouts, or integrating with design systems. |
Frontend Design
Overview
UI/UX implementation patterns covering component design, accessibility requirements, responsive design, and design system integration. Focus on building consistent, accessible, and maintainable interfaces.
When to Use
- Designing new UI components
- Implementing accessibility features
- Creating responsive layouts
- Integrating with design systems
- Styling with CSS/Tailwind
Quick Reference
| Aspect | Key Principles |
|---|---|
| Components | Single responsibility, composable, typed props |
| Accessibility | Semantic HTML, ARIA, keyboard nav, focus |
| Responsive | Mobile-first, breakpoints, fluid sizing |
| Design System | Tokens, consistent spacing, reusable |
Component Design Principles
Structure
// Good: Single responsibility, typed props
interface ButtonProps {
variant: 'primary' | 'secondary' | 'ghost';
size: 'sm' | 'md' | 'lg';
isLoading?: boolean;
disabled?: boolean;
children: React.ReactNode;
onClick?: () => void;
}
export function Button({
variant,
size,
isLoading = false,
disabled = false,
children,
onClick,
}: ButtonProps) {
return (
<button
className={cn(
buttonBase,
variants[variant],
sizes[size],
isLoading && 'opacity-50 cursor-wait'
)}
disabled={disabled || isLoading}
onClick={onClick}
>
{isLoading ? <Spinner /> : children}
</button>
);
}
Component Checklist
- Props are typed with TypeScript
- Default props for optional values
- Composable (accepts children or slots)
- Accessible (proper ARIA, keyboard)
- Responsive (works on all screen sizes)
- Tested (unit + visual)
Accessibility Requirements
WCAG 2.1 AA Essentials
| Requirement | Implementation |
|---|---|
| Color Contrast | 4.5:1 text, 3:1 large text/graphics |
| Focus Visible | Clear focus ring on all interactive elements |
| Keyboard Nav | All functions accessible via keyboard |
| Alt Text | Descriptive alt for images |
| Form Labels | Every input has associated label |
| Error Messages | Clear, associated with field |
Semantic HTML
<!-- Good: Semantic structure -->
<header>
<nav aria-label="Main navigation">
<ul role="list">
<li><a href="/home">Home</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>Page Title</h1>
<section aria-labelledby="section-heading">
<h2 id="section-heading">Section</h2>
</section>
</article>
</main>
<footer>...</footer>
ARIA Patterns
// Dialog/Modal
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-description">Are you sure?</p>
</div>
// Tab Panel
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel-1">Tab 1</button>
<button role="tab" aria-selected="false" aria-controls="panel-2">Tab 2</button>
</div>
<div role="tabpanel" id="panel-1">Content 1</div>
// Live Region (announcements)
<div aria-live="polite" aria-atomic="true">
{statusMessage}
</div>
Keyboard Navigation
// Focus trap for modals
useEffect(() => {
const focusableElements = modalRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusableElements?.[0];
const last = focusableElements?.[focusableElements.length - 1];
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last?.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first?.focus();
}
}
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);
Responsive Design Patterns
Mobile-First Breakpoints
/* Tailwind default breakpoints */
/* sm: 640px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px */
/* Mobile-first approach */
.container {
padding: 1rem; /* Mobile default */
}
@media (min-width: 768px) {
.container {
padding: 2rem; /* Tablet+ */
}
}
@media (min-width: 1024px) {
.container {
padding: 4rem; /* Desktop */
}
}
Tailwind Responsive
<div className={cn(
// Mobile first
"flex flex-col gap-4 p-4",
// Tablet
"md:flex-row md:gap-6 md:p-6",
// Desktop
"lg:gap-8 lg:p-8"
)}>
Common Patterns
// Responsive grid
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
// Hide/show by breakpoint
<nav className="hidden md:flex"> {/* Desktop nav */}
<button className="md:hidden"> {/* Mobile menu button */}
// Responsive typography
<h1 className="text-2xl md:text-4xl lg:text-5xl">
// Responsive spacing
<section className="py-8 md:py-12 lg:py-16">
Design System Integration
Design Tokens
// tokens.ts
export const tokens = {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
},
semantic: {
success: '#22c55e',
warning: '#f59e0b',
error: '#ef4444',
},
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
},
radius: {
sm: '0.25rem',
md: '0.5rem',
lg: '1rem',
full: '9999px',
},
};
Tailwind Config
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
primary: tokens.colors.primary,
success: tokens.colors.semantic.success,
},
spacing: tokens.spacing,
borderRadius: tokens.radius,
},
},
};
Consistent Component API
// All components follow same patterns
type Size = 'sm' | 'md' | 'lg';
type Variant = 'primary' | 'secondary' | 'ghost';
interface BaseProps {
size?: Size;
variant?: Variant;
className?: string;
}
// Button, Input, Card, etc. all use BaseProps
Common Patterns
Loading States
// Skeleton loader
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2" />
<div className="h-4 bg-gray-200 rounded w-1/2" />
</div>
// Spinner
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Error States
<div role="alert" className="bg-red-50 border-l-4 border-red-500 p-4">
<div className="flex">
<AlertIcon className="text-red-500" />
<div className="ml-3">
<p className="text-red-700 font-medium">Error</p>
<p className="text-red-600 text-sm">{errorMessage}</p>
</div>
</div>
</div>
Red Flags - STOP
Never:
- Use
divwith click handlers (usebutton) - Remove focus outlines without replacement
- Rely only on color to convey information
- Skip alt text on meaningful images
- Use fixed widths that break on mobile
Always:
- Test with keyboard navigation
- Check color contrast
- Provide focus indicators
- Use semantic HTML elements
- Test on multiple screen sizes
Integration
Related skills: react-patterns, nextjs-patterns Tools: Lighthouse, axe DevTools, WAVE