Frontend UI Engineering
Build production-quality UIs. Use when building or modifying user-facing interfaces, creating components, implementing layouts, or managing state.
Build production-quality UIs. Use when building or modifying user-facing interfaces, creating components, implementing layouts, or managing state.
Build production-quality user interfaces that are accessible, performant, and visually polished.
Colocate everything related to a component:
src/components/
TaskList/
TaskList.tsx # Component implementation
TaskList.test.tsx # Tests
TaskList.stories.tsx # Storybook stories
use-task-list.ts # Custom hook
types.ts # Component-specific types
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:
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>
);
}
Choose the simplest approach that works:
| State Type | Use Case | |------------|----------| | 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 |
Avoid prop drilling deeper than 3 levels.
| AI Default | Problem | Production Quality | |------------|---------|-------------------| | Purple/indigo everything | Models default to visually "safe" palettes | Use the project's actual color palette | | Excessive gradients | Adds visual noise | Flat or subtle gradients matching the design system | | Rounded everything | Maximum rounding ignores hierarchy | Consistent border-radius from the design system | | Generic hero sections | Template-driven layout | Content-first layouts | | Lorem ipsum-style copy | Hides layout problems | Realistic placeholder content | | Oversized padding everywhere | Destroys visual hierarchy | Consistent spacing scale |
// Every interactive element must be keyboard accessible
<button onClick={handleClick}>Click me</button> // ✓ Focusable by default
<div onClick={handleClick}>Click me</div> // ✗ Not focusable
// 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" />
function Dialog({ isOpen, onClose }: DialogProps) {
const closeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (isOpen) closeRef.current?.focus();
}, [isOpen]);
return (
<dialog open={isOpen}>
<button ref={closeRef} onClick={onClose}>Close</button>
</dialog>
);
}
Design for mobile first, then expand:
<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: 320px, 768px, 1024px, 1440px.
// 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>
);
}