2026-05-07 22:06:00 +02:00
---
name: frontend-ui-engineering
description: Builds production-quality UIs. Use when building or modifying user-facing interfaces. Use when creating components, implementing layouts, managing state, or when the output needs to look and feel production-quality rather than AI-generated.
---
# Frontend UI Engineering
## Overview
Build production-quality user interfaces that are accessible, performant, and visually polished. The goal is UI that looks like it was built by a design-aware engineer at a top company — not like it was generated by an AI. This means real design system adherence, proper accessibility, thoughtful interaction patterns, and no 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 related to a component:
```
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:**
```tsx
// 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:**
```tsx
// 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)} />
2026-05-07 22:08:00 +02:00
< span className = {task.done ? " line-through text-muted " : " " } >
{task.title}
< / span >
2026-05-07 22:06:00 +02:00
< Button variant = "ghost" size = "sm" onClick = {() = > onDelete(task.id)}>
< TrashIcon / >
< / Button >
< / li >
);
}
```
**Separate data fetching from presentation:**
```tsx
// Container: handles data
export function TaskListContainer() {
const { tasks, isLoading, error } = useTasks();
if (isLoading) return < TaskListSkeleton / > ;
2026-05-07 22:08:00 +02:00
if (error)
return < ErrorState message = "Failed to load tasks" retry = {refetch} / > ;
2026-05-07 22:06:00 +02:00
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" >
2026-05-07 22:08:00 +02:00
{tasks.map((task) => (
< TaskItem key = {task.id} task = {task} / >
))}
2026-05-07 22:06:00 +02:00
< / ul >
);
}
```
## State Management
**Choose the simplest approach 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
```
**Avoid prop drilling deeper than 3 levels.** If you're passing props through components that don't use them, introduce context or restructure the component tree.
## Design System Adherence
### Avoid the AI Aesthetic
AI-generated UI has recognizable patterns. Avoid all of them:
2026-05-07 22:08:00 +02:00
| AI Default | Why It Is a Problem | Production Quality |
| -------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| Purple/indigo everything | Models default to visually "safe" palettes, making every app look identical | Use the project's actual color palette |
| Excessive gradients | Gradients add visual noise and clash with most design systems | Flat or subtle gradients matching the design system |
| Rounded everything (rounded-2xl) | Maximum rounding signals "friendly" but ignores the hierarchy of corner radii in real designs | Consistent border-radius from the design system |
| Generic hero sections | Template-driven layout with no connection to the actual content or user need | Content-first layouts |
| Lorem ipsum-style copy | Placeholder text hides layout problems that real content reveals (length, wrapping, overflow) | Realistic placeholder content |
| Oversized padding everywhere | Equal generous padding destroys visual hierarchy and wastes screen space | Consistent spacing scale |
| Stock card grids | Uniform grids are a layout shortcut that ignores information priority and scanning patterns | Purpose-driven layouts |
| Shadow-heavy design | Layered shadows add depth that competes with content and slows rendering on low-end devices | Subtle or no shadows unless the design system specifies |
2026-05-07 22:06:00 +02:00
### Spacing and Layout
Use a consistent spacing scale. Don't invent values:
```css
/* Use the scale: 0.25rem increments (or whatever the project uses) */
2026-05-07 22:08:00 +02:00
/* 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 */
2026-05-07 22:06:00 +02:00
```
### Typography
Respect the type hierarchy:
```
h1 → Page title (one per page)
h2 → Section title
h3 → Subsection title
body → Default text
small → Secondary/helper text
```
Don't skip heading levels. Don't 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 normal text, 3:1 for large text)
- Don't rely solely on color to convey information (use icons, text, or patterns too)
## Accessibility (WCAG 2.1 AA)
Every component must meet these standards:
### Keyboard Navigation
```tsx
// 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
```tsx
// 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
```tsx
// 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} >
2026-05-07 22:08:00 +02:00
< button ref = {closeRef} onClick = {onClose} >
Close
< / button >
2026-05-07 22:06:00 +02:00
{/* dialog content */}
< / dialog >
);
}
```
### Meaningful Empty and Error States
```tsx
// 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 >
2026-05-07 22:08:00 +02:00
< 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 >
2026-05-07 22:06:00 +02:00
< / div >
);
}
return < ul role = "list" > ...< / ul > ;
}
```
## Responsive Design
Design for mobile first, then expand:
```tsx
// 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
```tsx
// 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) => {
2026-05-07 22:08:00 +02:00
await queryClient.cancelQueries({ queryKey: ["tasks"] });
const previous = queryClient.getQueryData(["tasks"]);
2026-05-07 22:06:00 +02:00
2026-05-07 22:08:00 +02:00
queryClient.setQueryData(["tasks"], (old: Task[]) =>
old.map((t) => (t.id === taskId ? { ...t, done: !t.done } : t)),
2026-05-07 22:06:00 +02:00
);
return { previous };
},
onError: (_err, _taskId, context) => {
2026-05-07 22:08:00 +02:00
queryClient.setQueryData(["tasks"], context?.previous);
2026-05-07 22:06:00 +02:00
},
});
}
```
## See Also
For detailed accessibility requirements and testing tools, see `references/accessibility-checklist.md` .
## Common Rationalizations
2026-05-07 22:08:00 +02:00
| Rationalization | Reality |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------- |
| "Accessibility is a nice-to-have" | It's a legal requirement in many jurisdictions and an engineering quality standard. |
| "We'll make it responsive later" | Retrofitting responsive design is 3x harder than building it from the start. |
2026-05-07 22:06:00 +02:00
| "The design isn't final, so I'll skip styling" | Use the design system defaults. Unstyled UI creates a broken first impression for reviewers. |
2026-05-07 22:08:00 +02:00
| "This is just a prototype" | Prototypes become production code. Build the foundation right. |
| "The AI aesthetic is fine for now" | It signals low quality. Use the project's actual design system from the start. |
2026-05-07 22:06:00 +02:00
## Red Flags
- Components with more than 200 lines (split them)
- Inline styles or arbitrary pixel values
- Missing error states, loading states, or empty states
- No keyboard navigation testing
- Color as the sole indicator of state (red/green without text or icons)
- Generic "AI look" (purple gradients, oversized cards, stock layouts)
## Verification
After building UI:
- [ ] Component renders without console errors
- [ ] All interactive elements are keyboard accessible (Tab through the page)
- [ ] Screen reader can convey the page's content and structure
- [ ] Responsive: works at 320px, 768px, 1024px, 1440px
- [ ] Loading, error, and empty states all handled
- [ ] Follows the project's design system (spacing, colors, typography)
- [ ] No accessibility warnings in dev tools or axe-core