Frontend UI engineering

Builds production-quality, accessible, responsive user-facing UIs.

How to use it

Claude Code
  1. Run the line below. It pulls the whole folder into ~/.claude/skills/frontend-ui-engineering, including the files SKILL.md points to.
  2. Describe your job in plain words. Claude Code follows the skill from there.
Claude Code — installs the whole folder, not just SKILL.md
npx degit addyosmani/agent-skills/skills/frontend-ui-engineering#main ~/.claude/skills/frontend-ui-engineering

For one project only, change the path to .claude/skills/frontend-ui-engineering. This skill also uses use-task-list.ts, types.ts — copying SKILL.md alone won't be enough. See the folder on GitHub.

Claude (web or desktop app)
  1. On this page open ⋯ → Download .md.
  2. Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
  3. Pick the file and Save. Claude shows the name and description and runs a security scan.
  4. Check the skill is switched on.
  5. Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
  1. ChatGPT: make a Project and paste it into Instructions.
  2. Neither? Paste it at the top of a new chat — it works for that chat.
Not working?
  • Check which app you pasted it into — the steps above name the right one.
  • Some skills need the paid tier of Claude or ChatGPT.
Step-by-step guide with screenshots · Ask in the forum

Paste into Claude, ChatGPT or Cursor.

Source of Frontend UI engineering

Show the full text341 lines
namedescription
frontend-ui-engineeringBuilds production-quality, accessible, responsive user-facing UIs. Use when building or modifying interfaces and pages, creating components, implementing layouts, meeting WCAG accessibility requirements, 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:

// 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

Reference-led UI quality

When the product needs a distinct visual direction, collect evidence before choosing a layout:

  1. Search a trusted reference catalogue or use references supplied by the product team.
  2. Study two or three relevant screens. Record decisions about hierarchy, density, navigation, controls, responsive behavior, and interaction states.
  3. Turn those decisions into a short design contract before implementation. Name the screen's job, primary action, required states, responsive rules, and patterns to reject.
  4. Rebuild the useful structure in the product's own components, tokens, content, and visual language. Never copy another product's branding, proprietary text, imagery, or exact layout.

Use references as evidence, not as templates. If references are unavailable, document the assumptions and verify the result against the product's existing design system.

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, 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
// 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, empty, error, success, and permission states handled when applicable
  • Follows the project's design system (spacing, colors, typography)
  • The rendered result passes a final UI-specific finish-gate review
  • No accessibility warnings in dev tools or axe-core
1---
2name: frontend-ui-engineering
3description: Builds production-quality, accessible, responsive user-facing UIs. Use when building or modifying interfaces and pages, creating components, implementing layouts, meeting WCAG accessibility requirements, managing state, or when the output needs to look and feel production-quality rather than AI-generated.
4---
5 
6# Frontend UI Engineering
7 
8## Overview
9 
10Build 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."
11 
12## When to Use
13 
14- Building new UI components or pages
15- Modifying existing user-facing interfaces
16- Implementing responsive layouts
17- Adding interactivity or state management
18- Fixing visual or UX issues
19 
20## Component Architecture
21 
22### File Structure
23 
24Colocate everything related to a component:
25 
26```
27src/components/
28 TaskList/
29 TaskList.tsx # Component implementation
30 TaskList.test.tsx # Tests
31 TaskList.stories.tsx # Storybook stories (if using)
32 use-task-list.ts # Custom hook (if complex state)
33 types.ts # Component-specific types (if needed)
34```
35 
36### Component Patterns
37 
38**Prefer composition over configuration:**
39 
40```tsx
41// Good: Composable
42<Card>
43 <CardHeader>
44 <CardTitle>Tasks</CardTitle>
45 </CardHeader>
46 <CardBody>
47 <TaskList tasks={tasks} />
48 </CardBody>
49</Card>
50 
51// Avoid: Over-configured
52<Card
53 title="Tasks"
54 headerVariant="large"
55 bodyPadding="md"
56 content={<TaskList tasks={tasks} />}
57/>
58```
59 
60**Keep components focused:**
61 
62```tsx
63// Good: Does one thing
64export function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {
65 return (
66 <li className="flex items-center gap-3 p-3">
67 <Checkbox checked={task.done} onChange={() => onToggle(task.id)} />
68 <span className={task.done ? 'line-through text-muted' : ''}>{task.title}</span>
69 <Button variant="ghost" size="sm" onClick={() => onDelete(task.id)}>
70 <TrashIcon />
71 </Button>
72 </li>
73 );
74}
75```
76 
77**Separate data fetching from presentation:**
78 
79```tsx
80// Container: handles data
81export function TaskListContainer() {
82 const { tasks, isLoading, error } = useTasks();
83 
84 if (isLoading) return <TaskListSkeleton />;
85 if (error) return <ErrorState message="Failed to load tasks" retry={refetch} />;
86 if (tasks.length === 0) return <EmptyState message="No tasks yet" />;
87 
88 return <TaskList tasks={tasks} />;
89}
90 
91// Presentation: handles rendering
92export function TaskList({ tasks }: { tasks: Task[] }) {
93 return (
94 <ul role="list" className="divide-y">
95 {tasks.map(task => <TaskItem key={task.id} task={task} />)}
96 </ul>
97 );
98}
99```
100 
101## State Management
102 
103**Choose the simplest approach that works:**
104 
105```
106Local state (useState) → Component-specific UI state
107Lifted state → Shared between 2-3 sibling components
108Context → Theme, auth, locale (read-heavy, write-rare)
109URL state (searchParams) → Filters, pagination, shareable UI state
110Server state (React Query, SWR) → Remote data with caching
111Global store (Zustand, Redux) → Complex client state shared app-wide
112```
113 
114**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.
115 
116## Design System Adherence
117 
118### Reference-led UI quality
119 
120When the product needs a distinct visual direction, collect evidence before choosing a layout:
121 
1221. Search a trusted reference catalogue or use references supplied by the product team.
1232. Study two or three relevant screens. Record decisions about hierarchy, density, navigation, controls, responsive behavior, and interaction states.
1243. Turn those decisions into a short design contract before implementation. Name the screen's job, primary action, required states, responsive rules, and patterns to reject.
1254. Rebuild the useful structure in the product's own components, tokens, content, and visual language. Never copy another product's branding, proprietary text, imagery, or exact layout.
126 
127Use references as evidence, not as templates. If references are unavailable, document the assumptions and verify the result against the product's existing design system.
128 
129### Avoid the AI Aesthetic
130 
131AI-generated UI has recognizable patterns. Avoid all of them:
132 
133| AI Default | Why It Is a Problem | Production Quality |
134|---|---|---|
135| Purple/indigo everything | Models default to visually "safe" palettes, making every app look identical | Use the project's actual color palette |
136| Excessive gradients | Gradients add visual noise and clash with most design systems | Flat or subtle gradients matching the design system |
137| 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 |
138| Generic hero sections | Template-driven layout with no connection to the actual content or user need | Content-first layouts |
139| Lorem ipsum-style copy | Placeholder text hides layout problems that real content reveals (length, wrapping, overflow) | Realistic placeholder content |
140| Oversized padding everywhere | Equal generous padding destroys visual hierarchy and wastes screen space | Consistent spacing scale |
141| Stock card grids | Uniform grids are a layout shortcut that ignores information priority and scanning patterns | Purpose-driven layouts |
142| 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 |
143 
144### Spacing and Layout
145 
146Use a consistent spacing scale. Don't invent values:
147 
148```css
149/* Use the scale: 0.25rem increments (or whatever the project uses) */
150/* Good */ padding: 1rem; /* 16px */
151/* Good */ gap: 0.75rem; /* 12px */
152/* Bad */ padding: 13px; /* Not on any scale */
153/* Bad */ margin-top: 2.3rem; /* Not on any scale */
154```
155 
156### Typography
157 
158Respect the type hierarchy:
159 
160```
161h1 → Page title (one per page)
162h2 → Section title
163h3 → Subsection title
164body → Default text
165small → Secondary/helper text
166```
167 
168Don't skip heading levels. Don't use heading styles for non-heading content.
169 
170### Color
171 
172- Use semantic color tokens: `text-primary`, `bg-surface`, `border-default` — not raw hex values
173- Ensure sufficient contrast (4.5:1 for normal text, 3:1 for large text)
174- Don't rely solely on color to convey information (use icons, text, or patterns too)
175 
176## Accessibility (WCAG 2.1 AA)
177 
178Every component must meet these standards:
179 
180### Keyboard Navigation
181 
182```tsx
183// Every interactive element must be keyboard accessible
184<button onClick={handleClick}>Click me</button> // ✓ Focusable by default
185<div onClick={handleClick}>Click me</div> // ✗ Not focusable
186<div role="button" tabIndex={0} onClick={handleClick} // ✓ But prefer <button>
187 onKeyDown={e => {
188 if (e.key === 'Enter') handleClick();
189 if (e.key === ' ') e.preventDefault();
190 }}
191 onKeyUp={e => {
192 if (e.key === ' ') handleClick();
193 }}>
194 Click me
195</div>
196```
197 
198### ARIA Labels
199 
200```tsx
201// Label interactive elements that lack visible text
202<button aria-label="Close dialog"><XIcon /></button>
203 
204// Label form inputs
205<label htmlFor="email">Email</label>
206<input id="email" type="email" />
207 
208// Or use aria-label when no visible label exists
209<input aria-label="Search tasks" type="search" />
210```
211 
212### Focus Management
213 
214```tsx
215// Move focus when content changes
216function Dialog({ isOpen, onClose }: DialogProps) {
217 const closeRef = useRef<HTMLButtonElement>(null);
218 
219 useEffect(() => {
220 if (isOpen) closeRef.current?.focus();
221 }, [isOpen]);
222 
223 // Trap focus inside dialog when open
224 return (
225 <dialog open={isOpen}>
226 <button ref={closeRef} onClick={onClose}>Close</button>
227 {/* dialog content */}
228 </dialog>
229 );
230}
231```
232 
233### Meaningful Empty and Error States
234 
235```tsx
236// Don't show blank screens
237function TaskList({ tasks }: { tasks: Task[] }) {
238 if (tasks.length === 0) {
239 return (
240 <div role="status" className="text-center py-12">
241 <TasksEmptyIcon className="mx-auto h-12 w-12 text-muted" />
242 <h3 className="mt-2 text-sm font-medium">No tasks</h3>
243 <p className="mt-1 text-sm text-muted">Get started by creating a new task.</p>
244 <Button className="mt-4" onClick={onCreateTask}>Create Task</Button>
245 </div>
246 );
247 }
248 
249 return <ul role="list">...</ul>;
250}
251```
252 
253## Responsive Design
254 
255Design for mobile first, then expand:
256 
257```tsx
258// Tailwind: mobile-first responsive
259<div className="
260 grid grid-cols-1 /* Mobile: single column */
261 sm:grid-cols-2 /* Small: 2 columns */
262 lg:grid-cols-3 /* Large: 3 columns */
263 gap-4
264">
265```
266 
267Test at these breakpoints: 320px, 768px, 1024px, 1440px.
268 
269## Loading and Transitions
270 
271```tsx
272// Skeleton loading (not spinners for content)
273function TaskListSkeleton() {
274 return (
275 <div className="space-y-3" aria-busy="true" aria-label="Loading tasks">
276 {Array.from({ length: 3 }).map((_, i) => (
277 <div key={i} className="h-12 bg-muted animate-pulse rounded" />
278 ))}
279 </div>
280 );
281}
282 
283// Optimistic updates for perceived speed
284function useToggleTask() {
285 const queryClient = useQueryClient();
286 
287 return useMutation({
288 mutationFn: toggleTask,
289 onMutate: async (taskId) => {
290 await queryClient.cancelQueries({ queryKey: ['tasks'] });
291 const previous = queryClient.getQueryData(['tasks']);
292 
293 queryClient.setQueryData(['tasks'], (old: Task[]) =>
294 old.map(t => t.id === taskId ? { ...t, done: !t.done } : t)
295 );
296 
297 return { previous };
298 },
299 onError: (_err, _taskId, context) => {
300 queryClient.setQueryData(['tasks'], context?.previous);
301 },
302 });
303}
304```
305 
306## See Also
307 
308For detailed accessibility requirements and testing tools, see `../../references/accessibility-checklist.md`.
309 
310## Common Rationalizations
311 
312| Rationalization | Reality |
313|---|---|
314| "Accessibility is a nice-to-have" | It's a legal requirement in many jurisdictions and an engineering quality standard. |
315| "We'll make it responsive later" | Retrofitting responsive design is 3x harder than building it from the start. |
316| "The design isn't final, so I'll skip styling" | Use the design system defaults. Unstyled UI creates a broken first impression for reviewers. |
317| "This is just a prototype" | Prototypes become production code. Build the foundation right. |
318| "The AI aesthetic is fine for now" | It signals low quality. Use the project's actual design system from the start. |
319 
320## Red Flags
321 
322- Components with more than 200 lines (split them)
323- Inline styles or arbitrary pixel values
324- Missing error states, loading states, or empty states
325- No keyboard navigation testing
326- Color as the sole indicator of state (red/green without text or icons)
327- Generic "AI look" (purple gradients, oversized cards, stock layouts)
328 
329## Verification
330 
331After building UI:
332 
333- [ ] Component renders without console errors
334- [ ] All interactive elements are keyboard accessible (Tab through the page)
335- [ ] Screen reader can convey the page's content and structure
336- [ ] Responsive: works at 320px, 768px, 1024px, 1440px
337- [ ] Loading, empty, error, success, and permission states handled when applicable
338- [ ] Follows the project's design system (spacing, colors, typography)
339- [ ] The rendered result passes a final UI-specific finish-gate review
340- [ ] No accessibility warnings in dev tools or axe-core
341 

Discussion

Alternatives

Also in AccessibilitySee all 106 in Design →