Agent-skills frontend-ui-engineering
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.
git clone https://github.com/addyosmani/agent-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/addyosmani/agent-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/frontend-ui-engineering" ~/.claude/skills/addyosmani-agent-skills-frontend-ui-engineering && rm -rf "$T"
skills/frontend-ui-engineering/SKILL.mdFrontend 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:
// 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:
// 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 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:
| 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 |
Spacing and Layout
Use a consistent spacing scale. Don't invent values:
/* 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 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
— not raw hex valuesborder-default - 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
// 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 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
Design for mobile first, then expand:
// 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 Rationalizations
| 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. |
| "The design isn't final, so I'll skip styling" | Use the design system defaults. Unstyled UI creates a broken first impression for reviewers. |
| "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. |
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