{"id":"2cd4d6a9-fb90-45a4-bd45-17a55e5ebda0","shortId":"V7yCMP","kind":"skill","title":"Frontend Ui Engineering","tagline":"Agent Skills skill by Addyosmani","description":"# Frontend UI Engineering\n\n## Overview\n\nBuild 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.\"\n\n## When to Use\n\n- Building new UI components or pages\n- Modifying existing user-facing interfaces\n- Implementing responsive layouts\n- Adding interactivity or state management\n- Fixing visual or UX issues\n\n## Component Architecture\n\n### File Structure\n\nColocate everything related to a component:\n\n```\nsrc/components/\n  TaskList/\n    TaskList.tsx          # Component implementation\n    TaskList.test.tsx     # Tests\n    TaskList.stories.tsx  # Storybook stories (if using)\n    use-task-list.ts      # Custom hook (if complex state)\n    types.ts              # Component-specific types (if needed)\n```\n\n### Component Patterns\n\n**Prefer composition over configuration:**\n\n```tsx\n// Good: Composable\n<Card>\n  <CardHeader>\n    <CardTitle>Tasks</CardTitle>\n  </CardHeader>\n  <CardBody>\n    <TaskList tasks={tasks} />\n  </CardBody>\n</Card>\n\n// Avoid: Over-configured\n<Card\n  title=\"Tasks\"\n  headerVariant=\"large\"\n  bodyPadding=\"md\"\n  content={<TaskList tasks={tasks} />}\n/>\n```\n\n**Keep components focused:**\n\n```tsx\n// Good: Does one thing\nexport function TaskItem({ task, onToggle, onDelete }: TaskItemProps) {\n  return (\n    <li className=\"flex items-center gap-3 p-3\">\n      <Checkbox checked={task.done} onChange={() => onToggle(task.id)} />\n      <span className={task.done ? 'line-through text-muted' : ''}>{task.title}</span>\n      <Button variant=\"ghost\" size=\"sm\" onClick={() => onDelete(task.id)}>\n        <TrashIcon />\n      </Button>\n    </li>\n  );\n}\n```\n\n**Separate data fetching from presentation:**\n\n```tsx\n// Container: handles data\nexport function TaskListContainer() {\n  const { tasks, isLoading, error } = useTasks();\n\n  if (isLoading) return <TaskListSkeleton />;\n  if (error) return <ErrorState message=\"Failed to load tasks\" retry={refetch} />;\n  if (tasks.length === 0) return <EmptyState message=\"No tasks yet\" />;\n\n  return <TaskList tasks={tasks} />;\n}\n\n// Presentation: handles rendering\nexport function TaskList({ tasks }: { tasks: Task[] }) {\n  return (\n    <ul role=\"list\" className=\"divide-y\">\n      {tasks.map(task => <TaskItem key={task.id} task={task} />)}\n    </ul>\n  );\n}\n```\n\n## State Management\n\n**Choose the simplest approach that works:**\n\n```\nLocal state (useState)           → Component-specific UI state\nLifted state                     → Shared between 2-3 sibling components\nContext                          → Theme, auth, locale (read-heavy, write-rare)\nURL state (searchParams)         → Filters, pagination, shareable UI state\nServer state (React Query, SWR)  → Remote data with caching\nGlobal store (Zustand, Redux)    → Complex client state shared app-wide\n```\n\n**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.\n\n## Design System Adherence\n\n### Avoid the AI Aesthetic\n\nAI-generated UI has recognizable patterns. Avoid all of them:\n\n| AI Default | Why It Is a Problem | Production Quality |\n|---|---|---|\n| Purple/indigo everything | Models default to visually \"safe\" palettes, making every app look identical | Use the project's actual color palette |\n| Excessive gradients | Gradients add visual noise and clash with most design systems | Flat or subtle gradients matching the design system |\n| 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 |\n| Generic hero sections | Template-driven layout with no connection to the actual content or user need | Content-first layouts |\n| Lorem ipsum-style copy | Placeholder text hides layout problems that real content reveals (length, wrapping, overflow) | Realistic placeholder content |\n| Oversized padding everywhere | Equal generous padding destroys visual hierarchy and wastes screen space | Consistent spacing scale |\n| Stock card grids | Uniform grids are a layout shortcut that ignores information priority and scanning patterns | Purpose-driven layouts |\n| 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 |\n\n### Spacing and Layout\n\nUse a consistent spacing scale. Don't invent values:\n\n```css\n/* Use the scale: 0.25rem increments (or whatever the project uses) */\n/* Good */  padding: 1rem;      /* 16px */\n/* Good */  gap: 0.75rem;       /* 12px */\n/* Bad */   padding: 13px;      /* Not on any scale */\n/* Bad */   margin-top: 2.3rem; /* Not on any scale */\n```\n\n### Typography\n\nRespect the type hierarchy:\n\n```\nh1 → Page title (one per page)\nh2 → Section title\nh3 → Subsection title\nbody → Default text\nsmall → Secondary/helper text\n```\n\nDon't skip heading levels. Don't use heading styles for non-heading content.\n\n### Color\n\n- Use semantic color tokens: `text-primary`, `bg-surface`, `border-default` — not raw hex values\n- Ensure sufficient contrast (4.5:1 for normal text, 3:1 for large text)\n- Don't rely solely on color to convey information (use icons, text, or patterns too)\n\n## Accessibility (WCAG 2.1 AA)\n\nEvery component must meet these standards:\n\n### Keyboard Navigation\n\n```tsx\n// Every interactive element must be keyboard accessible\n<button onClick={handleClick}>Click me</button>        // ✓ Focusable by default\n<div onClick={handleClick}>Click me</div>               // ✗ Not focusable\n<div role=\"button\" tabIndex={0} onClick={handleClick}    // ✓ But prefer <button>\n     onKeyDown={e => {\n       if (e.key === 'Enter') handleClick();\n       if (e.key === ' ') e.preventDefault();\n     }}\n     onKeyUp={e => {\n       if (e.key === ' ') handleClick();\n     }}>\n  Click me\n</div>\n```\n\n### ARIA Labels\n\n```tsx\n// Label interactive elements that lack visible text\n<button aria-label=\"Close dialog\"><XIcon /></button>\n\n// Label form inputs\n<label htmlFor=\"email\">Email</label>\n<input id=\"email\" type=\"email\" />\n\n// Or use aria-label when no visible label exists\n<input aria-label=\"Search tasks\" type=\"search\" />\n```\n\n### Focus Management\n\n```tsx\n// Move focus when content changes\nfunction Dialog({ isOpen, onClose }: DialogProps) {\n  const closeRef = useRef<HTMLButtonElement>(null);\n\n  useEffect(() => {\n    if (isOpen) closeRef.current?.focus();\n  }, [isOpen]);\n\n  // Trap focus inside dialog when open\n  return (\n    <dialog open={isOpen}>\n      <button ref={closeRef} onClick={onClose}>Close</button>\n      {/* dialog content */}\n    </dialog>\n  );\n}\n```\n\n### Meaningful Empty and Error States\n\n```tsx\n// Don't show blank screens\nfunction TaskList({ tasks }: { tasks: Task[] }) {\n  if (tasks.length === 0) {\n    return (\n      <div role=\"status\" className=\"text-center py-12\">\n        <TasksEmptyIcon className=\"mx-auto h-12 w-12 text-muted\" />\n        <h3 className=\"mt-2 text-sm font-medium\">No tasks</h3>\n        <p className=\"mt-1 text-sm text-muted\">Get started by creating a new task.</p>\n        <Button className=\"mt-4\" onClick={onCreateTask}>Create Task</Button>\n      </div>\n    );\n  }\n\n  return <ul role=\"list\">...</ul>;\n}\n```\n\n## Responsive Design\n\nDesign for mobile first, then expand:\n\n```tsx\n// Tailwind: mobile-first responsive\n<div className=\"\n  grid grid-cols-1      /* Mobile: single column */\n  sm:grid-cols-2        /* Small: 2 columns */\n  lg:grid-cols-3        /* Large: 3 columns */\n  gap-4\n\">\n```\n\nTest at these breakpoints: 320px, 768px, 1024px, 1440px.\n\n## Loading and Transitions\n\n```tsx\n// Skeleton loading (not spinners for content)\nfunction TaskListSkeleton() {\n  return (\n    <div className=\"space-y-3\" aria-busy=\"true\" aria-label=\"Loading tasks\">\n      {Array.from({ length: 3 }).map((_, i) => (\n        <div key={i} className=\"h-12 bg-muted animate-pulse rounded\" />\n      ))}\n    </div>\n  );\n}\n\n// Optimistic updates for perceived speed\nfunction useToggleTask() {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: toggleTask,\n    onMutate: async (taskId) => {\n      await queryClient.cancelQueries({ queryKey: ['tasks'] });\n      const previous = queryClient.getQueryData(['tasks']);\n\n      queryClient.setQueryData(['tasks'], (old: Task[]) =>\n        old.map(t => t.id === taskId ? { ...t, done: !t.done } : t)\n      );\n\n      return { previous };\n    },\n    onError: (_err, _taskId, context) => {\n      queryClient.setQueryData(['tasks'], context?.previous);\n    },\n  });\n}\n```\n\n## See Also\n\nFor detailed accessibility requirements and testing tools, see `references/accessibility-checklist.md`.\n\n## Common Rationalizations\n\n| Rationalization | Reality |\n|---|---|\n| \"Accessibility is a nice-to-have\" | It's a legal requirement in many jurisdictions and an engineering quality standard. |\n| \"We'll make it responsive later\" | Retrofitting responsive design is 3x harder than building it from the start. |\n| \"The design isn't final, so I'll skip styling\" | Use the design system defaults. Unstyled UI creates a broken first impression for reviewers. |\n| \"This is just a prototype\" | Prototypes become production code. Build the foundation right. |\n| \"The AI aesthetic is fine for now\" | It signals low quality. Use the project's actual design system from the start. |\n\n## Red Flags\n\n- Components with more than 200 lines (split them)\n- Inline styles or arbitrary pixel values\n- Missing error states, loading states, or empty states\n- No keyboard navigation testing\n- Color as the sole indicator of state (red/green without text or icons)\n- Generic \"AI look\" (purple gradients, oversized cards, stock layouts)\n\n## Verification\n\nAfter building UI:\n\n- [ ] Component renders without console errors\n- [ ] All interactive elements are keyboard accessible (Tab through the page)\n- [ ] Screen reader can convey the page's content and structure\n- [ ] Responsive: works at 320px, 768px, 1024px, 1440px\n- [ ] Loading, error, and empty states all handled\n- [ ] Follows the project's design system (spacing, colors, typography)\n- [ ] No accessibility warnings in dev tools or axe-core","tags":["frontend","engineering","agent","skills","addyosmani"],"capabilities":["skill","source-addyosmani","category-agent-skills"],"categories":["agent-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/addyosmani/agent-skills/frontend-ui-engineering","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"install_from":"skills.sh"}},"qualityScore":"0.300","qualityRationale":"deterministic score 0.30 from registry signals: · indexed on skills.sh · published under addyosmani/agent-skills","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill:v1","enrichmentVersion":1,"enrichedAt":"2026-04-22T11:40:29.478Z","embedding":null,"createdAt":"2026-04-18T20:31:07.111Z","updatedAt":"2026-04-22T11:40:29.478Z","lastSeenAt":"2026-04-22T11:40:29.478Z","tsv":"'-12':887 '-3':278 '-4':836 '0':234,718,822 '0.25':561 '0.75':575 '1':655,660 '1024px':862,1136 '12px':577 '13px':580 '1440px':863,1137 '16px':572 '1rem':571 '2':277 '2.1':681 '2.3':589 '200':1059 '2xl':416 '3':324,659,879 '320px':860,1134 '3x':987 '4.5':654 '768px':861,1135 'aa':682 'access':21,61,679,698,946,957,1116,1155 'actual':389,451,1047 'ad':88 'add':395,522 'addyosmani':8 'adher':59,347 'aesthet':69,351,1034 'agent':4 'ai':53,68,350,353,363,1033,1094 'ai-gener':352 'also':943 'anim':892 'animate-puls':891 'app':317,382 'app-wid':316 'approach':262 'arbitrari':1066 'architectur':99 'aria':739,756 'aria-label':755 'array.from':877 'async':910 'auth':283 'avoid':146,319,348,359 'await':912 'awar':40 'axe':1162 'axe-cor':1161 'bad':578,585 'becom':1025 'bg':642,889 'bg-mute':888 'bg-surfac':641 'blank':813 'bodi':612 'bodypad':155 'border':433,645 'border-default':644 'border-radius':432 'breakpoint':859 'broken':1014 'build':13,73,990,1028,1104 'built':35 'button':193,699,716,796,833 'cach':307 'card':150,497,1099 'category-agent-skills' 'chang':770 'check':178 'checkbox':177 'choos':259 'clash':399 'classnam':184,834,885 'click':702,710,737 'client':313 'close':801 'closeref':777,798 'closeref.current':783 'code':1027 'coloc':102 'color':390,633,636,669,1081,1152 'common':953 'compani':45 'compet':525 'complex':124,312 'compon':76,98,107,111,128,133,162,269,280,332,343,684,1055,1106 'component-specif':127,268 'compos':141 'composit':136 'configur':138,149 'connect':448 'consist':431,493,550 'consol':1109 'const':213,776,902,916 'contain':207 'content':157,452,457,472,479,527,632,769,803,873,1128 'content-first':456 'context':281,339,937,940 'contrast':653 'convey':671,1124 'copi':464 'core':1163 'corner':426 'creat':829,839,1012 'css':557 'custom':121 'data':202,209,305 'deeper':322 'default':364,375,613,646,706,1009 'depth':523 'design':39,57,345,402,410,430,437,519,542,843,844,985,996,1007,1048,1149 'design-awar':38 'destroy':486 'detail':945 'dev':1158 'devic':535 'dialog':772,789,793,802 'dialogprop':775 'div':707,714,882 'done':929 'drill':321 'driven':444,514 'e':724,733 'e.key':726,730,735 'e.preventdefault':731 'element':694,744,1113 'email':752 'empti':805,1075,1141 'end':534 'engin':3,11,41,974 'ensur':651 'enter':727 'equal':483 'err':935 'error':216,222,807,1070,1110,1139 'errorst':224 'everi':381,683,692 'everyth':103,373,413 'everywher':482 'excess':392 'exist':80,762 'expand':849 'export':169,210,243 'face':83 'fail':226 'fetch':203 'file':100 'filter':294 'final':999 'fine':1036 'first':458,847,854,1015 'fix':93 'flag':1054 'flat':404 'focus':163,704,713,763,767,784,787 'follow':1145 'form':750 'foundat':1030 'friend':420 'frontend':1,9 'function':170,211,244,771,815,874,900 'gap':574 'generat':50,354 'generic':67,439,1093 'generous':484 'get':826 'ghost':195 'global':308 'goal':27 'good':140,165,569,573 'gradient':393,394,407,1097 'grid':498,500 'h':886 'h1':600 'h2':606 'h3':609 'handl':208,241,1144 'handleclick':701,709,720,728,736 'harder':988 'head':621,626,631 'headervari':153 'heavi':287,518 'hero':440 'hex':649 'hide':467 'hierarchi':424,488,599 'hook':122 'icon':674,1092 'ident':384 'ignor':422,506 'implement':85,112 'impress':1016 'increment':563 'indic':1085 'inform':507,672 'inlin':1063 'input':751 'insid':788 'interact':63,89,693,743,1112 'interfac':18,84 'introduc':338 'invent':555 'ipsum':462 'ipsum-styl':461 'isload':215,219 'isn':997 'isopen':773,782,785,795 'issu':97 'jurisdict':971 'keep':161 'key':253,883 'keyboard':689,697,1078,1115 'label':740,742,749,757,761 'lack':746 'larg':154,662 'later':982 'layer':520 'layout':87,445,459,468,503,515,547,1101 'legal':967 'length':474,878 'level':325,622 'lift':273 'like':32,47 'line':187,1060 'line-through':186 'll':978,1002 'load':228,864,869,1072,1138 'local':265,284 'look':31,383,1095 'lorem':460 'low':533,1041 'low-end':532 'make':380,979 'manag':92,258,764 'mani':970 'map':880 'margin':587 'margin-top':586 'match':408 'maximum':417 'md':156 'mean':55 'meaning':804 'meet':686 'messag':225 'miss':1069 'mobil':846,853 'mobile-first':852 'model':374 'modifi':79 'move':766 'mt':835 'must':685,695 'mutationfn':907 'mute':191,890 'navig':690,1079 'need':132,455 'new':74,831 'nice':961 'nice-to-hav':960 'nois':397 'non':630 'non-head':629 'normal':657 'null':779 'old':922 'old.map':924 'onchang':180 'onclick':198,700,708,719,799,837 'onclos':774,800 'oncreatetask':838 'ondelet':174,199 'one':167,603 'onerror':934 'onkeydown':723 'onkeyup':732 'onmut':909 'ontoggl':173,181 'open':791,794 'optimist':895 'over-configur':147 'overflow':476 'overs':480,1098 'overview':12 'pad':481,485,570,579 'page':78,601,605,1120,1126 'pagin':295 'palett':379,391 'pass':329 'pattern':64,134,358,511,677 'per':604 'perceiv':898 'perform':22 'pixel':1067 'placehold':465,478 'polish':25 'prefer':135,722 'present':205,240 'previous':917,933,941 'primari':640 'prioriti':508 'problem':369,469 'product':15,370,1026 'production-qu':14 'project':387,567,1045,1147 'prop':320,330 'proper':60 'prototyp':1023,1024 'puls':893 'purpl':1096 'purple/indigo':372 'purpos':513 'purpose-driven':512 'qualiti':16,371,975,1042 'queri':302 'querycli':903 'queryclient.cancelqueries':913 'queryclient.getquerydata':918 'queryclient.setquerydata':920,938 'querykey':914 'radii':427 'radius':434 'rare':290 'ration':954,955 'raw':648 're':328 'react':301 'read':286 'read-heavi':285 'reader':1122 'real':56,429,471 'realist':477 'realiti':956 'recogniz':357 'red':1053 'red/green':1088 'redux':311 'ref':797 'references/accessibility-checklist.md':952 'refetch':231 'relat':104 'reli':666 'rem':562,576,590 'remot':304 'render':242,530,1107 'requir':947,968 'respect':596 'respons':86,842,855,981,984,1131 'restructur':341 'retri':230 'retrofit':983 'return':176,220,223,235,236,249,792,823,841,876,905,932 'reveal':473 'review':1018 'right':1031 'role':715 'round':412,415,418,894 'rounded-2xl':414 'safe':378 'scale':495,552,560,584,594 'scan':510 'screen':491,814,1121 'searchparam':293 'secondary/helper':616 'section':441,607 'see':942,951 'semant':635 'separ':201 'server':299 'shadow':517,521,539 'shadow-heavi':516 'share':275,315 'shareabl':296 'shortcut':504 'show':812 'sibl':279 'signal':419,1040 'simplest':261 'size':196 'skeleton':868 'skill':5,6 'skip':620,1003 'slow':529 'sm':197 'small':615 'sole':667,1084 'source-addyosmani' 'space':492,494,545,551,1151 'span':183 'specif':129,270 'specifi':544 'speed':899 'spinner':871 'split':1061 'src/components':108 'standard':688,976 'start':827,994,1052 'state':91,125,257,266,272,274,292,298,300,314,808,1071,1073,1076,1087,1142 'stock':496,1100 'store':309 'stori':117 'storybook':116 'structur':101,1130 'style':463,627,1004,1064 'subsect':610 'subtl':406,536 'suffici':652 'surfac':643 'swr':303 'system':58,346,403,411,438,543,1008,1049,1150 't.done':930 't.id':926 'tab':1117 'tabindex':717 'tailwind':851 'task':142,144,145,152,159,160,172,214,229,238,239,246,247,248,251,255,256,817,818,819,825,832,840,915,919,921,923,939 'task.done':179,185 'task.id':182,200,254 'task.title':192 'taskid':911,927,936 'taskitem':171,252 'taskitemprop':175 'tasklist':109,143,158,237,245,816 'tasklist.stories.tsx':115 'tasklist.test.tsx':113 'tasklist.tsx':110 'tasklistcontain':212 'tasklistskeleton':875 'tasks.length':233,821 'tasks.map':250 'templat':443 'template-driven':442 'test':114,856,949,1080 'text':190,466,614,617,639,658,663,675,748,1090 'text-mut':189 'text-primari':638 'theme':282 'thing':168 'thought':62 'titl':151,602,608,611 'toggletask':908 'token':637 'tool':950,1159 'top':44,588 'transit':866 'trap':786 'tree':344 'tsx':139,164,206,691,741,765,809,850,867 'type':130,598 'types.ts':126 'typographi':595,1153 'ui':2,10,29,75,271,297,355,1011,1105 'uniform':499 'unless':540 'unstyl':1010 'updat':896 'url':291 'use':72,119,336,385,548,558,568,625,634,673,754,1005,1043 'use-task-list.ts':120 'useeffect':780 'usemut':906 'usequerycli':904 'user':17,82,454 'user-fac':81 'useref':778 'usest':267 'usetask':217 'usetoggletask':901 'ux':96 'valu':556,650,1068 'variant':194 'verif':1102 'visibl':747,760 'visual':24,94,377,396,487 'warn':1156 'wast':490 'wcag':680 'whatev':565 'wide':318 'without':1089,1108 'work':264,1132 'wrap':475 'write':289 'write-rar':288 'zustand':310","prices":[{"id":"f449cffb-e1df-4870-9b4d-2bd605fbc792","listingId":"2cd4d6a9-fb90-45a4-bd45-17a55e5ebda0","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"addyosmani","category":"agent-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T20:31:07.111Z"}],"sources":[{"listingId":"2cd4d6a9-fb90-45a4-bd45-17a55e5ebda0","source":"github","sourceId":"addyosmani/agent-skills/frontend-ui-engineering","sourceUrl":"https://github.com/addyosmani/agent-skills/tree/main/skills/frontend-ui-engineering","isPrimary":false,"firstSeenAt":"2026-04-18T21:52:59.765Z","lastSeenAt":"2026-04-22T06:52:42.270Z"},{"listingId":"2cd4d6a9-fb90-45a4-bd45-17a55e5ebda0","source":"skills_sh","sourceId":"addyosmani/agent-skills/frontend-ui-engineering","sourceUrl":"https://skills.sh/addyosmani/agent-skills/frontend-ui-engineering","isPrimary":true,"firstSeenAt":"2026-04-18T20:31:07.111Z","lastSeenAt":"2026-04-22T11:40:29.478Z"}],"details":{"listingId":"2cd4d6a9-fb90-45a4-bd45-17a55e5ebda0","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"addyosmani","slug":"frontend-ui-engineering","source":"skills_sh","category":"agent-skills","skills_sh_url":"https://skills.sh/addyosmani/agent-skills/frontend-ui-engineering"},"updatedAt":"2026-04-22T11:40:29.478Z"}}