Overview
Build accessible, high-performance, visually polished production-grade user interfaces. The goal is to make the UI look like it was built by a design-conscious engineer at a top-tier company, not AI-generated. This means genuinely following the design system, correct accessibility, thoughtful interaction patterns, and avoiding that generic "AI aesthetic".
When to Use
- Building new UI components or pages
- Modifying existing user-facing interfaces
- Implementing responsive layouts
- Adding interactivity or state management
- Fixing visual or UX issues
Component Architecture
File Structure
Colocate everything component-related together:
src/components/
TaskList/
TaskList.tsx # Component implementation
TaskList.test.tsx # Tests
TaskList.stories.tsx # Storybook stories (if using)
use-task-list.ts # Custom hook (if complex state)
types.ts # Component-specific types (if needed)
Component Patterns
Prefer composition over configuration:
// Good: Composable
<Card>
<CardHeader>
<CardTitle>Tasks</CardTitle>
</CardHeader>
<CardBody>
<TaskList tasks={tasks} />
</CardBody>
</Card>
// Avoid: Over-configured
<Card
title="Tasks"
headerVariant="large"
bodyPadding="md"
content={<TaskList tasks={tasks} />}
/>
Keep components focused in responsibility:
// Good: Does one thing
export function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
return (
<li className="flex items-center gap-3 p-3">
<Checkbox checked={task.done} onChange={() => onToggle(task.id)} />
<span className={task.done ? 'line-through text-muted' : ''}>{task.title}</span>
<Button variant="ghost" size="sm" onClick={() => onDelete(task.id)}>
<TrashIcon />
</Button>
</li>
);
}
Separate data fetching from presentation:
// Container: handles data
export function TaskListContainer() {
const { tasks, isLoading, error } = useTasks();
if (isLoading) return <TaskListSkeleton />;
if (error) return <ErrorState message="Failed to load tasks" retry={refetch} />;
if (tasks.length === 0) return <EmptyState message="No tasks yet" />;
return <TaskList tasks={tasks} />;
}
// Presentation: handles rendering
export function TaskList({ tasks }: { tasks: Task[] }) {
return (
<ul role="list" className="divide-y">
{tasks.map(task => <TaskItem key={task.id} task={task} />)}
</ul>
);
}
State Management
Choose the simplest solution that works:
Local state (useState) → Component-specific UI state
Lifted state → Shared between 2-3 sibling components
Context → Theme, auth, locale (read-heavy, write-rare)
URL state (searchParams) → Filters, pagination, shareable UI state
Server state (React Query, SWR) → Remote data with caching
Global store (Zustand, Redux) → Complex client state shared app-wide
Do not let props pass through more than 3 levels. If you are passing props layer by layer between components that do not use them, introduce context or refactor the component tree.
Follow the Design System
Avoid the AI Aesthetic
AI-generated UI has recognizable patterns. Avoid all of them:
| AI Default | Why It Is a Problem | Production-Grade Standard |
|---|---|---|
| Full-screen purple/indigo | Models default to visually "safe" color schemes, making every app look the same | Use the project's actual color palette |
| Overuse of gradients | Gradients add visual noise and conflict with most design systems | Flat or restrained gradients that match the design system |
| Everything rounded (rounded-2xl) | Maximum rounding conveys "friendliness" but ignores the hierarchy of corner radii in real design | Follow consistent border-radius in the design system |
| Cookie-cutter hero sections | Template-driven layouts with no connection to actual content and user needs | Content-first layouts |
| Lorem ipsum-style copy | Placeholder text hides layout issues that only real content would expose (length, line breaks, overflow) | Realistic placeholder content |
| Excessive padding everywhere | Uniformly loose padding destroys visual hierarchy and wastes screen space | Consistent spacing scale |
| Stock card grids | Homogeneous grids are a layout shortcut that ignore information priority and scanning patterns | Goal-driven layouts |
| Heavy shadow design | Layer upon layer of shadows adds depth that competes with content for attention and slows rendering on low-end devices | Use restrained or no shadows unless the design system explicitly specifies otherwise |
Spacing and Layout
Use a consistent spacing scale; do not invent values out of thin air:
/* Use the scale: 0.25rem increments (or whatever the project uses) */
/* Good */ padding: 1rem; /* 16px */
/* Good */ gap: 0.75rem; /* 12px */
/* Bad */ padding: 13px; /* Not on any scale */
/* Bad */ margin-top: 2.3rem; /* Not on any scale */
Typography
Respect heading hierarchy:
h1 → Page title (one per page)
h2 → Section title
h3 → Subsection title
body → Default text
small → Secondary/helper text
Do not skip heading levels. Do not use heading styles for non-heading content.
Color
- Use semantic color tokens:
text-primary、bg-surface、border-default, not raw hex values - Ensure sufficient contrast (4.5:1 for body text, 3:1 for large text)
- Do not convey information by color alone (also use icons, text, or patterns)
Accessibility (WCAG 2.1 AA)
Every component must meet these standards:
Keyboard navigation
// Every interactive element must be keyboard accessible
<button onClick={handleClick}>Click me</button> // ✓ Focusable by default
<div onClick={handleClick}>Click me</div> // ✗ Not focusable
<div role="button" tabIndex={0} onClick={handleClick} // ✓ But prefer <button>
onKeyDown={e => {
if (e.key === 'Enter') handleClick();
if (e.key === ' ') e.preventDefault();
}}
onKeyUp={e => {
if (e.key === ' ') handleClick();
}}>
Click me
</div>
ARIA labels
// Label interactive elements that lack visible text
<button aria-label="Close dialog"><XIcon /></button>
// Label form inputs
<label htmlFor="email">Email</label>
<input id="email" type="email" />
// Or use aria-label when no visible label exists
<input aria-label="Search tasks" type="search" />
Focus management
// Move focus when content changes
function Dialog({ isOpen, onClose }: DialogProps) {
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) closeRef.current?.focus();
}, [isOpen]);
// Trap focus inside dialog when open
return (
<dialog open={isOpen}>
<button ref={closeRef} onClick={onClose}>Close</button>
{/* dialog content */}
</dialog>
);
}
Meaningful empty states and error states
// Don't show blank screens
function TaskList({ tasks }: { tasks: Task[] }) {
if (tasks.length === 0) {
return (
<div role="status" className="text-center py-12">
<TasksEmptyIcon className="mx-auto h-12 w-12 text-muted" />
<h3 className="mt-2 text-sm font-medium">No tasks</h3>
<p className="mt-1 text-sm text-muted">Get started by creating a new task.</p>
<Button className="mt-4" onClick={onCreateTask}>Create Task</Button>
</div>
);
}
return <ul role="list">...</ul>;
}
Responsive design
Mobile-first design, then scale up to larger screens:
// Tailwind: mobile-first responsive
<div className="
grid grid-cols-1 /* Mobile: single column */
sm:grid-cols-2 /* Small: 2 columns */
lg:grid-cols-3 /* Large: 3 columns */
gap-4
">
Test at these breakpoints: 320px, 768px, 1024px, 1440px.
Loading and transitions
// Skeleton loading (not spinners for content)
function TaskListSkeleton() {
return (
<div className="space-y-3" aria-busy="true" aria-label="Loading tasks">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="h-12 bg-muted animate-pulse rounded" />
))}
</div>
);
}
// Optimistic updates for perceived speed
function useToggleTask() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: toggleTask,
onMutate: async (taskId) => {
await queryClient.cancelQueries({ queryKey: ['tasks'] });
const previous = queryClient.getQueryData(['tasks']);
queryClient.setQueryData(['tasks'], (old: Task[]) =>
old.map(t => t.id === taskId ? { ...t, done: !t.done } : t)
);
return { previous };
},
onError: (_err, _taskId, context) => {
queryClient.setQueryData(['tasks'], context?.previous);
},
});
}
See also
For detailed accessibility requirements and testing tools, see ../../references/accessibility-checklist.md。
Common rationalization excuses
| Excuse | Reality |
|---|---|
| "Accessibility is a nice-to-have" | In many jurisdictions it is a legal requirement, and it is also an engineering quality standard. |
| "We'll make it responsive later" | Retrofitting responsive design is 3 times harder than doing it right from the start. |
| "The design isn't finalized yet, so skip styling for now" | Use the design system defaults. An unstyled UI gives reviewers a broken first impression. |
| "This is just a prototype" | Prototypes become production code. Get the foundation right. |
| "The AI aesthetic is fine for now" | It signals low quality. Use the project's real design system from the start. |
Red flags
- Components over 200 lines (split them)
- Inline styles or arbitrary pixel values
- Missing error states, loading states, or empty states
- No keyboard navigation testing
- Using color as the only indicator of state (red/green, with no text or icon)
- A generic "AI look" (purple gradients, oversized cards, stock layouts)
Verification
After building the UI:
- No console errors when components render
- All interactive elements are keyboard accessible (navigate the page with the Tab key)
- Screen readers can convey the page's content and structure
- Responsive: works correctly at 320px, 768px, 1024px, 1440px
- Loading, error, and empty states are all handled
- Follows the project's design system (spacing, color, typography)
- No accessibility warnings in dev tools or axe-core