Skip to content
Back to skills

Frontend Ui Engineering

ASecurity

Build production-quality, accessible, non-generic-looking UI components and pages — component composition, the state-management ladder (local to global), avoiding the recognizable "AI aesthetic," WCAG 2.1 AA accessibility, and responsive/loading/empty states. Use when building or modifying user-facing interfaces, or when output needs to look like it was built by a design-aware engineer rather than generated by a model.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 16, 2026
developmentgoreactvuetestingapifrontend

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 16, 2026

npx -y skills add shennawardana23/skillme --skill frontend-ui-engineering --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Frontend Ui Engineering?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Frontend Ui Engineering
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/shennawardana23-frontend-ui-engineering/badge)](https://www.skillsdirectory.com/skills/shennawardana23-frontend-ui-engineering)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: frontend-ui-engineering
description: Build production-quality, accessible, non-generic-looking UI components and pages — component composition, the state-management ladder (local to global), avoiding the recognizable "AI aesthetic," WCAG 2.1 AA accessibility, and responsive/loading/empty states. Use when building or modifying user-facing interfaces, or when output needs to look like it was built by a design-aware engineer rather than generated by a model.
license: Apache-2.0
metadata:
  version: "0.1.0"
---

# Frontend UI Engineering

Production-quality UI is accessible, follows the project's actual design
system, and handles every state (loading, empty, error) — not just the happy
path. Examples below use Vue 3 Composition API syntax; the underlying
component-architecture and accessibility principles apply to any framework.

## When to use

Building new UI components or pages, modifying existing user-facing
interfaces, implementing responsive layouts, adding interactivity, or fixing
visual/UX issues.

## Component architecture

**Colocate everything related to one component:**

```
components/TaskList/
  TaskList.vue
  TaskList.spec.ts
  useTaskList.ts     # composable, if state logic is non-trivial
  types.ts
```

**Composition over configuration.** A component that accepts children/slots
adapts to unforeseen content; a component with a growing prop surface for
every variant becomes unmaintainable:

```vue
<!-- Good: composable -->
<Card>
  <CardHeader><CardTitle>Tasks</CardTitle></CardHeader>
  <CardBody><TaskList :tasks="tasks" /></CardBody>
</Card>

<!-- Avoid: over-configured -->
<Card
  title="Tasks"
  header-variant="large"
  body-padding="md"
  :content="tasksContent"
/>
```

**Keep components single-purpose:**

```vue
<script setup lang="ts">
defineProps<{ task: Task }>();
const emit = defineEmits<{ toggle: [id: string]; delete: [id: string] }>();
</script>

<template>
  <li class="flex items-center gap-3 p-3">
    <input type="checkbox" :checked="task.done" @change="emit('toggle', task.id)" />
    <span :class="task.done ? 'line-through text-muted' : ''">{{ task.title }}</span>
    <button aria-label="Delete task" @click="emit('delete', task.id)"><TrashIcon /></button>
  </li>
</template>
```

**Separate data fetching from presentation** — a container component owns
loading/error/empty branching; the presentation component only renders data
it's already been handed. This keeps the presentation half trivially
testable and reusable in a different data-fetching context (e.g. Storybook,
a different route).

## The state-management ladder

Pick the least powerful tool that covers the sharing radius — escalating
unnecessarily makes state harder to trace, not easier:

| Need | Tool |
|---|---|
| Component-local UI state | `ref`/`reactive` in `<script setup>` |
| Shared between 2-3 sibling components | Lift to the nearest common parent |
| Read-heavy, write-rare (theme, auth, locale) | `provide`/`inject` |
| Filters, pagination, shareable UI state | URL query params (`useRoute`/`useRouter`) |
| Remote data with caching | `useFetch`/`useAsyncData` (Nuxt) |
| Complex state shared app-wide | Pinia store |

Avoid passing a prop through more than about three layers of components that
don't themselves use it — that's the concrete signal to introduce
`provide`/`inject` or a store instead of drilling further.

## Avoiding the generic "AI aesthetic"

Model-generated UI has recognizable, interchangeable defaults. Avoid all of
them in favor of the project's actual design system:

| Default pattern | Why it's a problem | Do instead |
|---|---|---|
| Purple/indigo everything | A "safe" default that makes every app look identical | Use the project's real palette |
| Excessive gradients | Visual noise that clashes with most design systems | Flat or subtle, matching the system |
| Rounded everything (`rounded-2xl` on all elements) | Ignores the real hierarchy of corner radii | Use the system's radius scale |
| Generic hero sections | Template layout unconnected to actual content | Content-first layout |
| Lorem-ipsum placeholder copy | Hides real layout problems (wrapping, overflow, length) | Realistic placeholder content |
| Oversized uniform padding | Destroys visual hierarchy | Consistent spacing scale |
| Stock card grids | Ignores information priority and scanning order | Purpose-driven layout |
| Shadow-heavy design | Competes with content, costs paint time | Subtle or no shadows unless specified |

The test behind every row above: a choice is suspect when it could be
pasted into an unrelated product unchanged, or communicates no useful
information, state, action, hierarchy, or brand meaning. Name it, ask
whether removing it improves clarity, and default to deleting it over
adding a new effect to compensate.

This table is the build-time checklist. To grade UI that's already
shipped — a point-in-time audit with a cited, prioritized findings report
instead of a build guide — use `ai-design-slop-audit`.

**Spacing**: use the project's scale, never an invented value —
`padding: 1rem` (on-scale) not `padding: 13px` (off-scale, arbitrary).

**Typography**: respect hierarchy — one `h1` per page, sequential heading
levels, never a heading style applied to non-heading content for visual
effect.

**Color**: use semantic tokens (`text-primary`, `bg-surface`) not raw hex;
meet WCAG 2.1 AA contrast (4.5:1 normal text, 3:1 large text); never convey
state through color alone — pair it with an icon or text label.

## Accessibility (WCAG 2.1 AA)

**Keyboard access**: every interactive element must be reachable and
operable by keyboard. Prefer a native `<button>` — it's focusable and
Enter/Space-activatable for free. If a non-native element must act as a
control, give it `role`, `tabindex="0"`, and explicit key handling:

```vue
<button @click="handleClick">Click me</button>                 <!-- preferred -->
<div role="button" tabindex="0" @click="handleClick"
     @keydown.enter="handleClick" @keydown.space.prevent="handleClick">
  Click me
</div>
```

**Labels**: label every interactive element that has no visible text
(`aria-label="Close dialog"` on an icon-only button); pair every form input
with a `<label for>` or `aria-label`.

**Focus management**: move focus into a dialog when it opens, and restore
focus to the triggering element when it closes:

```vue
<script setup lang="ts">
const closeButton = ref<HTMLButtonElement>();
const props = defineProps<{ isOpen: boolean }>();
watch(() => props.isOpen, (open) => { if (open) closeButton.value?.focus(); });
</script>
```

**Empty and error states are content, not an afterthought:**

```vue
<template v-if="tasks.length === 0">
  <div role="status" class="text-center py-12">
    <h3>No tasks</h3>
    <p>Get started by creating a new task.</p>
    <button @click="onCreateTask">Create task</button>
  </div>
</template>
```

See `references/accessibility-checklist.md` for the full per-component
checklist (forms, modals, live regions, testing tools) — read it before a
final accessibility pass on a non-trivial component.

## Responsive design

Design mobile-first, then expand:

```css
.grid { grid-template-columns: 1fr; }
@media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .grid { grid-template-columns: repeat(3, 1fr); } }
```

Test at minimum: 320px, 768px, 1024px, 1440px.

## Loading, empty, and optimistic states

Use skeletons for content loading, not spinners — a skeleton communicates
approximate shape and reduces perceived wait; mark it `aria-busy="true"` so
assistive tech knows content is pending, not empty:

```vue
<div class="space-y-3" aria-busy="true" aria-label="Loading tasks">
  <div v-for="i in 3" :key="i" class="h-12 bg-muted animate-pulse rounded" />
</div>
```

For mutations users expect to feel instant (toggling a checkbox, reordering
a list), apply the change to local state immediately and roll it back only
if the server rejects it — an **optimistic update with rollback on error**.
The exact caching mechanism depends on your data layer (Nuxt's
`useAsyncData`, Pinia, or a TanStack Query-style cache); the pattern is the
same regardless: show the change now, reconcile against the server response
in the background, and revert visibly if it fails.

## Gotchas

- A component under ~200 lines is a rough ceiling, not a hard rule — if a
  single-file component is growing past that, it's usually mixing container
  and presentation concerns and should split.
- Inline styles and arbitrary pixel values (`margin-top: 13px`) are the most
  common way a design system silently erodes — they don't show up in a
  visual diff of one component, only in the aggregate inconsistency.
- "We'll make it responsive later" is materially more expensive than
  building mobile-first from the start, because retrofitting means
  re-deriving the layout logic for a viewport the component was never
  structured for.
- A missing empty state isn't a cosmetic gap — a blank screen with zero data
  and zero explanation reads as broken, not as "nothing here yet."
- Color as the *only* signal for state (red/green with no icon or text)
  fails for colorblind users and fails WCAG regardless of contrast ratio.

## Real-world grounding

The WCAG 2.1 AA contrast thresholds (4.5:1 normal text, 3:1 large text/UI
components) cited above are the W3C's published, testable success criteria
(1.4.3 and 1.4.11) — not house style, and the basis most accessibility
audit tools (axe-core, Lighthouse) check against directly.

## Verification checklist

- [ ] Renders without console errors
- [ ] Every interactive element is keyboard-reachable and operable (Tab through it)
- [ ] Screen reader conveys structure and state (via accessibility tree, not just visually)
- [ ] Responsive at 320px, 768px, 1024px, 1440px
- [ ] Loading, error, and empty states are all implemented, not just the happy path
- [ ] Matches the project's actual spacing/color/typography scale
- [ ] No axe-core or Lighthouse accessibility warnings

Files in this skill

  • SKILL.md9.8 KB
  • evals/evals.json3.3 KB
  • references/accessibility-checklist.md3.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.