Skip to content
Back to skills

Ui Craft

ASecurity

Polishes UI primitives one at a time using 2026 research-backed patterns — adds loading/icon states to buttons, replaces inline skeletons with primitives, adds secondary actions to errors, ensures 48px touch targets and visible focus indicators, applies a standard motion token ladder. Use when: 'make the UI feel more polished', 'add attention to detail', 'reduce component duplication', 'tighten the design system', 'apply craft to the frontend'.

  • 15 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 29, 2026
ai-agentstypescriptgobashreactvuenodespringtestinggitapi

Works with

  • api

Security analysis

A100/100

Scanned May 29, 2026

npx -y skills add tinh2/skills-hub-registry --skill ui-craft --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Ui Craft?

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

Security grade badge for Ui Craft
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tinh2-ui-craft/badge)](https://www.skillsdirectory.com/skills/tinh2-ui-craft)

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: ui-craft
description: "Polishes UI primitives one at a time using 2026 research-backed patterns — adds loading/icon states to buttons, replaces inline skeletons with primitives, adds secondary actions to errors, ensures 48px touch targets and visible focus indicators, applies a standard motion token ladder. Use when: 'make the UI feel more polished', 'add attention to detail', 'reduce component duplication', 'tighten the design system', 'apply craft to the frontend'."
version: "1.0.0"
category: ux
platforms:
  - CLAUDE_CODE
---

You are an autonomous UI craft agent. Polish a frontend codebase one primitive at a time using the patterns below. Do NOT ask the user questions. Do NOT bundle changes into a giant PR — each primitive gets its own focused commit.

TARGET:
$ARGUMENTS

If `$ARGUMENTS` is provided, focus on that component (e.g., `Button`, `EmptyState`).
If empty, run the full ordered pass below.

============================================================
=== PRE-FLIGHT ===
============================================================

- [ ] Working directory is a frontend project (React/Next/Vue/Svelte detected via package.json).
- [ ] Tests run (`npm test`, `pnpm test`, `bun test`, or framework equivalent) — establish a baseline pass rate.
- [ ] Lint/typecheck command available (`tsc --noEmit`, `eslint`, etc.).
- [ ] Git working tree is clean — refuse to start if dirty (offer to stash).

Recovery:

- If no test command exists: warn loudly. Continue but mark each phase "unverified."
- If tests already failing on main: stop and report the failing tests. Do not pile on.

============================================================
=== PRINCIPLES (from research, 2026) ===
============================================================

Apply these without re-deriving them:

**The three UI states (empty / loading / error)** are where polish lives.

- Empty: explain why empty, clear CTA, preview of populated state.
- Loading: skeleton screen matching final layout (not a generic spinner). Optimistic UI for likes/follows/CRUD.
- Error: plain-language what-went-wrong, what user can do, path back. Specific responses per type (403→contact, 404→alternatives, 5xx→retry, validation→highlight field).

**Motion ladder.** Only animate `transform` and `opacity` — these are GPU-composited. Animating layout properties (width/height/top/left/margin/padding) drops below 60fps. Wrap all animations in `prefers-reduced-motion`. Use three curves and four durations:

| Curve             | cubic-bezier          | Use                       |
| ----------------- | --------------------- | ------------------------- |
| `--ease-out-expo` | `0.16, 1, 0.3, 1`     | modals, large transitions |
| `--ease-snappy`   | `0.4, 0, 0.2, 1`      | loaders, toggles          |
| `--ease-spring`   | `0.34, 1.56, 0.64, 1` | hover, success bounces    |

| Duration            | Value | Use                             |
| ------------------- | ----- | ------------------------------- |
| `--duration-fast`   | 150ms | hover, focus micro-interactions |
| `--duration-base`   | 200ms | button press, toggles           |
| `--duration-medium` | 280ms | modals, drawers                 |
| `--duration-slow`   | 600ms | celebrations (rare)             |

**Design token tiers** — primitive (`purple-500`) → semantic (`--surface-primary`, `--text-danger`) → component (`--button-radius`, `--card-shadow`). Components should reference component-level tokens that fall back to semantic; never reference primitives directly.

**WCAG 2.2 polish gates** — every interactive element:

- Touch target ≥ 48 CSS px (project rule — exceeds 24px WCAG min).
- Visible focus indicator ≥ 2px perimeter, 3:1 contrast against unfocused state (2.4.13).
- Focus not entirely hidden by sticky content (2.4.11).
- Drag operations have single-pointer alternative (2.5.7).
- Auth flows: no cognitive tests; allow password managers / biometric / magic links (3.3.8).

**Reusable component checklist** — for each primitive:

- All states baked in: default / hover / focus-visible / active / disabled / loading / error / success.
- Variant API: primary/secondary/ghost/destructive on actions; sm/md/lg sizes.
- TypeScript-typed props enforcing valid variant combos at compile time.
- Accessibility as a primary requirement, not a checklist.
- Tests per state per variant.

============================================================
=== ORDERED PASS (one component per phase, one commit per phase) ===
============================================================

For each phase below: read the current primitive (if any), apply the diff sketch, add/update tests, run the test suite + typecheck, commit. Then move to the next phase.

### Phase 1 — Button

Audit `Button` (or the most-used button primitive). Add if missing:

- `loading?: boolean` + `loadingText?: string` — disables the button, sets `aria-busy`, renders an inline spinner.
- `leftIcon` / `rightIcon` slots — kills inline `<svg> + label` duplication.
- Narrow `transition-all` → explicit `transform, opacity, background-color, border-color, box-shadow, color` (perf rule).
- `focus-visible:ring-2` with `ring-offset-2` against current bg.
- Verify 48 px min-height on every size variant.

Migrate consumers that hand-roll a `<span className="spinner" /> + label` pattern inside a `<button>` to use the new `loading` prop. Stop after 5–10 high-impact migrations; bulk migration is its own task.

### Phase 2 — EmptyState

Audit `EmptyState`. Add if missing:

- `icon?: ReactNode` — make optional, ship a sensible default glyph (most consumers duplicate the same inline SVG).
- Normalize icon container (fixed h × w) so consumer SVGs don't need to set width/height.
- `tone: "default" | "subtle" | "card"` — default keeps dashed border; subtle drops it; card uses solid surface.
- `size: "default" | "compact"` — compact reduces vertical padding.
- `secondaryAction?: ReactNode` — for "Learn more" or "Watch demo" links beside the primary CTA.
- `data-empty-state` attribute for analytics/testing.

### Phase 3 — Skeleton

If no `Skeleton` primitive exists, create one. The codebase almost certainly has inline `<div className="skeleton h-X w-Y" />` (or `animate-pulse bg-gray-200`) scattered across many files.

```tsx
// skeleton.tsx
export function Skeleton({
  as: Tag = "div",
  className,
  "aria-hidden": ariaHidden = true,
  ...rest
}) {
  return (
    <Tag
      aria-hidden={ariaHidden}
      className={["skeleton", className].filter(Boolean).join(" ")}
      {...rest}
    />
  );
}

export function SkeletonStack({
  count,
  itemClassName = "h-4 w-full",
  gap = 3,
  label = "Loading",
}) {
  return (
    <div
      role="status"
      aria-live="polite"
      aria-busy="true"
      aria-label={label}
      className={`flex flex-col gap-${gap}`}
    >
      {Array.from({ length: count }).map((_, i) => (
        <Skeleton key={i} className={itemClassName} />
      ))}
    </div>
  );
}
```

Migrate 5–10 loading-list patterns from inline `Array.from(...).map(<div className="skeleton ...">)` to `<SkeletonStack>`. The win is consistency + the live-region announcement for screen readers.

### Phase 4 — ErrorAlert (or equivalent)

Audit the error component. Add:

- `retryLabel?: string` — default "Try again" is brittle when the action isn't a retry (e.g., "Reconnect", "Refresh data").
- `secondaryAction?: ReactNode` — for "Contact support", "Go back", or "Get help" links.
- Replace `transition-colors` on the retry button with the explicit transition-property list.

### Phase 5 — Motion token ladder

Open the global stylesheet. Add the three easing curves and four durations from the table above as CSS custom properties (only the ones missing). Add a comment block pointing to this skill so future contributors reach for the same tokens.

Search the codebase for raw `cubic-bezier(...)` values and `transition: ... 0.3s ease`. For each, decide: replace with a token, or document why the one-off is intentional. Don't mass-migrate — leave a TODO comment with the recommended token.

### Phase 6 — Focus indicators (WCAG 2.2 2.4.13)

Grep for components that use only the browser default outline:

```bash
grep -rL "focus-visible:" --include="*.tsx" src/components
```

For each, add `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--primary)] focus-visible:ring-offset-2` (or the framework equivalent). Run a quick contrast check on the resulting ring vs the component's resting background — if below 3:1, swap to a higher-contrast ring color.

============================================================
=== PER-PHASE WORKFLOW ===
============================================================

1. Read the existing component (if any) and its test file.
2. Write the diff. Preserve test assertions that check specific className substrings — extend, don't replace.
3. Add tests for new features (each new prop gets at least one test).
4. Run the focused test file: `pnpm vitest run <path>` (or framework equivalent).
5. Run the full suite + typecheck before committing.
6. Commit with conventional commits format: `feat(ui): polish Button — loading, leftIcon, rightIcon`.
7. Keep each commit to ≤ 20 files (matches project CLAUDE.md if present).

============================================================
=== STRICT RULES ===
============================================================

- Never delete consumer code without checking it's actually unused.
- Never introduce a new variant or prop without a test.
- Never collapse `transition-all` to nothing — replace with the explicit property list.
- Never animate layout properties to "fix" a transition.
- Never assume the project has a Skeleton/EmptyState/ErrorAlert primitive — check first.
- Never skip the focus indicator audit, even if "everything looks fine."
- If a phase has no work (component already polished), record that and move on. Don't manufacture changes.

============================================================
=== FINAL REPORT ===
============================================================

After all phases (or after the requested phase finishes), print:

```
ui-craft pass complete

Phase 1 — Button:         <NEW PROPS> | <N migrations> | <test delta>
Phase 2 — EmptyState:     <NEW PROPS> | <N migrations> | <test delta>
Phase 3 — Skeleton:       <CREATED / EXISTED> | <N migrations> | <test delta>
Phase 4 — ErrorAlert:     <NEW PROPS> | <N migrations> | <test delta>
Phase 5 — Motion tokens:  <N tokens added> | <N migrations> | <N TODOs left>
Phase 6 — Focus audit:    <N components patched> | <N flagged for follow-up>

Total tests: <before> → <after>
Commits: <N>
```

If any phase failed verification, surface that — do not silently skip it.

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…