# Frontend UI Engineering

> Builds production-quality, accessible, performant user interfaces. Applies to React/TypeScript frontends — verify stack before invoking; teams using other stacks should treat the principles (state-management ladder, AI-aesthetic avoidance, WCAG 2.1 AA) as transferable but the code patterns as React-specific. Use when building or modifying user-facing components, managing UI state, implementing layouts, or when output needs to look hand-built rather than AI-generated.

- Skill: `jankneumann/frontend-ui-engineering` (Agent Skill)
- Install (CLI): `npx skillmds@latest add jankneumann/frontend-ui-engineering`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jankneumann/frontend-ui-engineering/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: jankneumann (https://skillmd.com/u/jankneumann)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/jankneumann/frontend-ui-engineering

---


# Frontend UI Engineering

> **Stack note:** This skill ships with React/TypeScript examples because that is the reference stack used across these methodology skills. Teams on Vue, Svelte, Solid, Angular, or server-rendered stacks (Rails/Django/Phoenix) should treat the **principles** (component composition, the state-management ladder, AI-aesthetic avoidance, WCAG 2.1 AA discipline) as fully transferable, but treat the **code patterns** (`useState`, `React.memo`, `useMutation`, JSX) as React-specific. Before invoking, verify the project's stack — if it is not React, take the principles and translate the patterns to the local idiom rather than copying JSX literally.

## 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 (every variant requires a new prop)
<Card
  title="Tasks"
  headerVariant="large"
  bodyPadding="md"
  content={<TaskList tasks={tasks} />}
/>
```

**Keep components focused — one job each:**

```tsx
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:**

```tsx
// Container: handles data
export function TaskListContainer() {
  const { tasks, isLoading, error, refetch } = 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>
  );
}
```

The container component never returns the same JSX in two branches; the presentation component never calls a fetch hook.

## State Management — Decision Ladder

Choose the simplest tier that works. Climb only when the current tier fails for a concrete reason. Going straight to a global store is the most common mistake.

| Tier | Tool | When to use |
|---|---|---|
| 1. **Local** | `useState`, `useReducer` | Component-specific UI state — toggles, hover, form draft. |
| 2. **Lifted** | parent `useState` + props | Shared between 2-3 sibling components. |
| 3. **Context** | `React.Context` | Read-heavy, write-rare values: theme, auth identity, locale, feature flags. |
| 4. **URL** | `searchParams`, route state | Filters, pagination, tab selection, sort order — anything that should survive reload or be shareable as a link. |
| 5. **Server** | React Query, SWR, RTK Query | Remote data with caching, refetching, optimistic updates. **Most "global" state is actually server state.** |
| 6. **Global** | Zustand, Redux, Jotai | Complex client state shared app-wide that *isn't* server state (rare; e.g. cross-cutting wizard progress, draft autosave). |

**Avoid prop drilling deeper than 3 levels.** If you're passing props through components that don't use them, climb to the right tier (usually context or server-state cache) — not a global store.

**Decision shortcut for new state:**

1. Is this data fetched from a server? → **Server state (Tier 5).** Stop. Don't put it in Redux.
2. Should this state survive reload or be shareable via URL? → **URL state (Tier 4).**
3. Is it used only by this component? → **Local (Tier 1).**
4. Is it used by 2-3 siblings? → **Lifted (Tier 2).**
5. Is it cross-cutting and read-heavy? → **Context (Tier 3).**
6. None of the above? → **Global (Tier 6).** Justify in a comment.

## Design System Adherence

### Avoid the "AI Aesthetic"

AI-generated UI has recognizable tells. Avoid all of them:

| AI Default | Why It's 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 system. |
| `rounded-2xl` on everything | 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 during development. |
| Oversized padding everywhere | Equal generous padding destroys visual hierarchy and wastes screen space, especially on mobile. | Consistent spacing scale; tighter where information density matters. |
| 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. |
| Emoji bullet points in product UI | Signals "AI demo," not "real product." | Icons from the design system, or no decoration. |

### Spacing and Layout

Use a consistent spacing scale. Don't invent values:

```css
/* Good: on the project's 4px scale */
padding: 1rem;       /* 16px */
gap: 0.75rem;        /* 12px */

/* Bad: arbitrary pixel values */
padding: 13px;
margin-top: 2.3rem;
```

### 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 (no `<h1>` jumping straight to `<h4>`). Don't use heading styles for non-heading content (use a font utility class instead).

### 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 or non-text UI).
- Don't rely solely on color to convey information (pair red/green with icons or text).

## Accessibility (WCAG 2.1 AA)

This section covers the bar every component must clear. For the deeper checklist (forms, focus management, screen-reader testing, motion preferences), see `references/accessibility-checklist.md`.

### Keyboard Navigation

Every interactive element must be reachable with `Tab` and operable with the keyboard.

```tsx
// Native semantics: focusable + Enter/Space activation for free
<button onClick={handleClick}>Click me</button>

// Custom: requires manual tabIndex + key handling. Prefer <button>.
<div
  role="button"
  tabIndex={0}
  onClick={handleClick}
  onKeyDown={(e) => {
    if (e.key === 'Enter') handleClick();
    if (e.key === ' ') e.preventDefault();
  }}
  onKeyUp={(e) => {
    if (e.key === ' ') handleClick();
  }}
>
  Click me
</div>
```

### ARIA Labels

```tsx
// Icon-only button — give it an accessible name
<button aria-label="Close dialog"><XIcon /></button>

// Form inputs — programmatic label via <label for>
<label htmlFor="email">Email</label>
<input id="email" type="email" />

// Or aria-label when no visible label exists
<input aria-label="Search tasks" type="search" />
```

### Focus Management

```tsx
function Dialog({ isOpen, onClose }: DialogProps) {
  const closeRef = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    if (isOpen) closeRef.current?.focus();
  }, [isOpen]);

  return (
    <dialog open={isOpen} onClose={onClose}>
      <button ref={closeRef} onClick={onClose}>Close</button>
      {/* dialog content; focus trap inside */}
    </dialog>
  );
}
```

### Meaningful Empty and Error States

Don't ship blank screens.

```tsx
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 mobile-first, then expand:

```tsx
<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: 320px, 768px, 1024px, 1440px. The 320px check catches the most layout bugs.

## Loading and Transitions

```tsx
// Skeletons (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<Task[]>(['tasks']);
      queryClient.setQueryData<Task[]>(['tasks'], (old) =>
        (old ?? []).map((t) => (t.id === taskId ? { ...t, done: !t.done } : t))
      );
      return { previous };
    },
    onError: (_err, _taskId, context) => {
      if (context?.previous) queryClient.setQueryData(['tasks'], context.previous);
    },
  });
}
```

## Common Rationalizations

| Rationalization | Why it's wrong |
|---|---|
| "Accessibility is a nice-to-have" | It's a legal requirement in many jurisdictions and a baseline engineering quality standard. |
| "We'll make it responsive later" | Retrofitting responsive design is 3x harder than building it from the start; layout assumptions get baked into every child component. |
| "The design isn't final, so I'll skip styling" | Use the design system defaults. Unstyled UI creates a broken first impression for reviewers and hides real layout issues. |
| "This is just a prototype" | Prototypes become production code. Build the foundation right; you won't get a second pass. |
| "The AI aesthetic is fine for now" | It signals low quality and dates instantly. Use the project's actual design system from the first commit. |
| "I'll put it in Redux to be safe" | Most "global" state is server state; reach for React Query / SWR before a client-side store. The state ladder exists for a reason. |
| "Lorem ipsum is fine for layout" | Real content reveals wrapping, length, overflow, and i18n bugs that lorem ipsum hides. Use representative content. |

## Red Flags

- Components longer than ~200 lines (split them into smaller pieces).
- Inline styles or arbitrary pixel values that aren't on the design system's spacing scale.
- Missing loading, error, or empty states (only the happy path is rendered).
- No keyboard test in the PR description; reviewer can't tell whether `Tab` actually works.
- Color as the sole indicator of state (red/green without text or icons).
- Generic "AI look": purple gradients, oversized cards, `rounded-2xl` everywhere, stock hero section.
- New state went straight into a global store without trying local/lifted/context/URL/server first.
- `useEffect` fetching data instead of a server-state library — almost always a bug source (race conditions, stale data, no caching).
- A `<div onClick>` doing the job of a `<button>`.

## Verification

1. The component renders without console errors or React warnings on first paint.
2. Every interactive element is reachable via `Tab` in a logical order, and `Enter`/`Space` activate it. Confirm by tabbing through the page yourself.
3. Loading, error, and empty states are all implemented and visible in Storybook or a manual demo route.
4. The component has been viewed at 320px, 768px, 1024px, and 1440px without horizontal scroll or clipped content.
5. State for this component sits at the lowest tier of the state ladder that works (cite which tier and why).
6. No values from the AI-aesthetic table appear: no `rounded-2xl` on everything, no purple/indigo defaults, no oversized padding, no lorem-ipsum strings shipped.
7. axe-core / browser dev-tools accessibility panel reports zero serious or critical violations on the rendered output.
8. The deeper accessibility checks in `references/accessibility-checklist.md` were run against this component (forms, focus, motion preferences as applicable).

