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