Nuxt Frontend Design
Full lifecycle frontend skill: setup, build, polish. Detects the right phase from project state.
Worktree isolation
Before any edit, follow the worktree isolation contract.
Job artifacts, contract format, dev server, checkpointing, and the handoff schema live in references/workflow.md. Read it before Phase 2 or Phase 3.
Project State
!bash "${CLAUDE_SKILL_DIR}/scripts/project-state.sh"
Phase Detection
$ARGUMENTS is the text after the invocation, e.g. /nuxt-frontend-design frost landing page → "frost landing page". Parse it for both a theme name and an action; that example means ensure the frost theme, then build the landing page.
IS_NUXT=false: this skill needs a Nuxt project (nuxt.config.ts). Say so and stop.
| State | Phase |
|---|---|
| HAS_COLORS / HAS_THEME / HAS_GUIDELINES all false | Phase 1 (setup) |
| Mixed (some true, some false) | Phase 1 (repair a corrupted setup) |
$ARGUMENTS names a theme the design system doesn't use |
Phase 1 with that theme |
| PRIOR_REVIEW with FAIL verdict | Repair pass (below) |
$ARGUMENTS asks for new pages |
Phase 2 (build) |
$ARGUMENTS asks to refine/polish, or existing pages need work |
Phase 3 (polish) |
| No arguments, design system exists | Phase 3 (audit existing pages) |
Repair pass: read the full {JOB_DIR}/review-report.md, not just the verdict line. Fix every [HARD REJECT] and [RUBRIC] item at its referenced file:line, then re-emit the handoff with updated contract_criteria_status. Reuse the existing job ID.
Phase 2 depends on a complete design system. If any of the three indicators is missing, run Phase 1 first and tell the user you're doing so.
After Phase 1, emit the handoff and continue into Phase 2 when pages were asked for. Offer a fresh conversation with /nuxt-frontend-design {page} only after repair passes or for multi-page builds.
Content & Asset Rules
Apply across Phase 2 and Phase 3. Review treats violations as hard rejects.
Every Element Earns Its Place
Each visible element does one job:
- Inform: data the reader acts on or decides with.
- Teach: something the reader does not know yet and needs now.
- Orient: navigation, wayfinding, current location.
- Brand: an element the site's
DESIGN.mdregisters as a brand asset.
If an element has no job, delete it. Noise is any element that fails this test.
Show, don't tell. If a sentence describes what the UI does, show the UI doing it with a real component, real data, or a working demo. This applies to dashboards and empty states too.
No text decoration. These fail the test unless DESIGN.md registers them as brand:
- Eyebrows: a small label above a heading.
- Subtitles or taglines that restate the heading.
- Labels that restate the value ("Status: Active" becomes "Active").
- Counts or facts that do not vary ("Watching 3 of 3 sites"). For table columns, see Hierarchy in craft-rules.md.
- Decorative icons beside headings, cards, or list items. A per-metric icon that encodes its data, a trend direction or a status, informs. The same constant icon on every card carries no data and fails.
- Section numbers where the order means nothing.
- Metadata lines joined with "·" that nobody acts on.
If a section reads flat after removal, fix it with separation, scale, or layout. Do not add the decoration back. Sanctioned replacements: a data line below the heading, a hairline with a directional action, numbering where order is real, emphasis on one lead item.
Start with less. Build the first version with the fewest elements. Each addition names its job. Never pad with placeholder sections, dummy stats, filler copy, icon grids, or logo bars. Ask before you add content the contract did not name.
Subtraction pass. Before handoff, list each visible element with its job. Delete the ones with none. Show before and after screenshots.
AI Slop Ban List
Hard rejections unless the user explicitly asked for them:
- Aggressive gradient backgrounds (purple/blue hero gradients, full-page meshes)
- Cards with a coloured left-border accent stripe
- Illustrations drawn inline with SVG shapes (use a real asset or a labelled placeholder)
- Emoji in UI copy unless the brand system registers emoji as a token
- Generic stat blocks ("10M+ users, 99.9% uptime, 24/7 support")
- Overused sans stacks: Inter, Roboto, Open Sans, Lato, Montserrat, Arial, any bare
system-ui/sans-seriffallback as the only font - SaaS-landing cliché serifs (Fraunces, Playfair, DM Serif) used without commitment, i.e. paired with a generic sans for body copy as decoration only. A theme that genuinely commits to an editorial, paper-craft, or literary aesthetic (kinetic-paper, flow, monograph) is right to use an editorial serif. The ban targets "add a serif heading for personality" tokenism.
Placeholder Over Fake
A labelled placeholder beats a bad attempt at the real thing.
- Missing hero image: a neutral block reading
600×400 hero image, not a hand-drawn SVG mountain - Missing avatars:
https://api.dicebear.com/9.x/initials/svg?seed={name}orhttps://ui-avatars.com/api/?name={name} - Missing product screenshots: solid block at the real aspect ratio with a short label
- Missing logos:
https://logo.clearbit.com/{domain}for real brands, otherwise a labelled box
Never leave broken <img> tags or unstyled fallback boxes. Placeholders should look intentional.
Scope: placeholders suit internal tools, dashboards, and prototypes. Landing and marketing pages want real product demos, screenshots, and named logos per "Show, don't tell" above; a labelled grey box on a hero kills the sale. Fall back to placeholders only when real assets genuinely don't exist yet.
Minimum Scales
Surface these in the build contract as testable criteria.
- Body copy ≥ 16px; never below 14px
- Mobile tap targets ≥ 44×44px (buttons, links, icon triggers, form controls)
- Form input height ≥ 40px desktop, ≥ 44px mobile
- Contrast: WCAG AA for all text against its background
Phase 1: Design System Setup
Establish the visual foundation. Needed when no design system exists or a new theme is requested. No job ID: design system artifacts are shared, not job-scoped.
Design Philosophy
Commit to an aesthetic direction that serves the content before coding. Define one design principle as a tradeoff statement: "We prioritize X over Y" (e.g. "readability over visual impact", "tactile depth over minimalism"). Record it in DESIGN.md.
What to set up
- Color palette: primary, neutral, accent via
app.config.ts - Typography: font selection, registration via Nuxt Fonts,
--font-*in@theme - Icon system: collection matching the aesthetic via
@nuxt/icon - Semantic overrides:
--ui-bg,--ui-text,--ui-radiusfor light/dark - Component theming: global overrides via
app.config.tsslots/variants - Extended colors: registering custom theme colors in
nuxt.config.ts
Override, don't invent. Use Nuxt UI's existing tokens (bg-muted, text-default, --ui-bg, --ui-radius) and Tailwind's built-in utilities (shadow-md, rounded-xl). Customize by overriding those in app.config.ts and main.css rather than creating generic duplicates. Before naming a new token, check whether an existing --ui-* variable or Tailwind utility already covers it.
Theme signature tokens are the exception: when a theme's voice depends on an effect Tailwind can't express (--shadow-brutal, --shadow-clay, --shadow-paper, --shadow-glass, mesh/glow variables), define it. The ban targets generic duplicates (--shadow-soft, --radius-card), not theme-specific voice.
References
- design-system.md: full setup guide (colors, fonts, tokens, component theming)
- themes/{name}.md: complete theme implementation files
Available themes: frost, clay, blueprint, nebula, zen, neon, teenage-engineering, kinetic-paper, flow, devtool, monograph (the live list is injected above).
Read exactly one theme file: ${CLAUDE_SKILL_DIR}/references/themes/{chosen-theme}.md. Reading several for comparison burns context without improving the choice. With no theme specified, default to frost for dark mode projects and zen for light mode; ask only when the choice is genuinely ambiguous.
After Setup: Emit DESIGN.md
Emit DESIGN.md at the project root after writing app.config.ts, main.css, and nuxt.config.ts:
- Collision guard: if
DESIGN.mdexists, ask before overwriting. Offer to merge (keeping the decisions log) or abort. - Read the template: templates/DESIGN.md
- YAML front matter: the chosen theme file already carries a DESIGN.md-compatible YAML block. Copy it, then apply user-specific customisations (brand name, swapped primary hex, additional registered colours, extra component surfaces). Prefer token refs (
{colors.primary},{rounded.md}) over restating hex/px values. - Prose sections: fill every section below the front matter from the decisions just made, no placeholders.
- Write to
DESIGN.mdat the project root. - Verify placeholders removed:
grep -cE '\{\{|TODO|placeholder' DESIGN.mdreturns 0. (Single-brace{colors.primary}token refs are legitimate and stay.) - Verify tokens lint: run
pnpm dlx @google/design.md lint DESIGN.mdwithout hiding stderr or piping away its exit status. Require a successful exit and zero reported errors. If the linter crashes, preserve the diagnostic and repair its input before retrying. Forraw.match is not a function, quote floats and convertrgba()values to 8-digit hex. Record intentional button-primary contrast tradeoffs indocs/design-decisions.md. - Verify budget:
DESIGN.mdis at or under 300 lines, and every section is at or under 40. The caps and the audit live in the root docs contract.
Modifying an existing design system: update DESIGN.md in place.
DESIGN.md binds craft-rules.md to the project's components. It never restates those rules.
After Setup: COPY.md is the other half
DESIGN.md owns the pixels. It does not own the words, and a page built with no decision about
its words ships whatever sentence each agent liked that day.
After emitting DESIGN.md, check for COPY.md at the project root. If the project has
user-visible strings and no COPY.md, say so and offer to run the
installed Brundlefly copywriting Skill init workflow. Do not write one from inside
this skill: it bootstraps from the strings that already ship, and that is a different harvest
from the one done here.
When COPY.md exists, read it before writing a single label, heading, empty state or error, and
keep DESIGN.md pointing at it rather than restating voice rules. DESIGN.md's Voice section
is one line: which file owns this.
Setup Recovery
Phase 1 failing mid-setup (build error, malformed config) means fixing the config before anything else. Verify all three indicators pass (colors: in app.config.ts, @theme in main.css, DESIGN.md with no placeholders) before moving to Phase 2. A DESIGN.md with unfilled template sections is worse than none.
Setup Gotchas
- Font selection: avoid the top 5 web fonts (Inter, Roboto, Open Sans, Lato, Montserrat) and system-ui. Pick fonts that reinforce the design principle.
- Button visibility: primary CTAs need visible backgrounds. Set
colors.primaryplusbutton.defaultVariants.variant: 'solid'inapp.config.ts. Secondary actions (icon triggers, overflow menus, table row actions) can useghostoroutline; see components/feedback.md. - No fake UI: if it looks interactive, wire it up.
Phase 2: Build Pages
Compose pages and UI patterns with Nuxt UI v4+. Requires a design system.
Run the dev server setup and contract steps in references/workflow.md before the first smoke test.
Build recovery: build-progress.md present without build-handoff.json means a previous build was interrupted. Read the progress file for which pages completed and which criteria remain, resume from the last incomplete page, and re-run dev server setup since the old process is gone.
Read the Foundation
If not already in context from Phase 1:
- Read the
DESIGN.mdcore. - Read the topic file its load map names for the layer you work in. Skip the others.
- Read
COPY.md. - Grep
GLOSSARY.mdfor each noun you will render. Do not read it whole. - Open
main.css,app.config.tsornuxt.config.tsonly to look up a token you need.
Every string the page renders comes from COPY.md and GLOSSARY.md, in that order: the noun
from the glossary, the sentence around it from the copy file. Inventing either while building a
component is how a product ends up describing itself four ways.
Use the project's semantic tokens, fonts, and component overrides. Never hardcode colors, shadows, or radii that bypass the design system. For a design variation, override an existing Nuxt UI token rather than introducing a new one.
References
Load the reference matching what you're building. Loading several before writing any code costs context without improving the first page; pull the next one when you reach a new component type.
| Building… | Reference |
|---|---|
| Landing / marketing page | pages/landing.md |
| Dashboard / admin | pages/dashboard.md |
| Forms | components/forms.md |
| Data tables | components/tables.md |
| Navigation | components/navigation.md |
| Feedback / overlays | components/feedback.md |
| Charts, dashboards, sparklines, stat cards | polish/data-viz.md |
| Lists, tables, async states, custom CSS classes | craft-rules.md |
When The User Asks For Variations
For 2+ options of the same page or component, vary across concrete dimensions rather than producing near-duplicates. Pick at least three of:
- Scale: hero type at 48px vs 96px vs 144px
- Fill vs outline: solid surfaces vs bordered/ghost
- Layout structure: stacked vs split vs asymmetric grid
- Visual rhythm: uniform cards vs feature + supporting mix
- Type treatment: display serif vs utility sans, tight vs wide tracking
- Texture and layering: flat vs shadowed vs offset/overlapping
- Density: airy vs compact
Start the first variation by-the-book, then escalate: each successive variation pushes further on one or two axes. Three variations differing only in accent colour are the same design three times.
Expose variations via route (/pages/index-a.vue, /pages/index-b.vue) or a ?variant= query param with v-if blocks so the user can A/B them in one tab.
Build Gotchas
Framework traps that grep won't reveal:
- v4 component renames:
ButtonGroup→UFieldGroup,PageMarquee→UMarquee. Old names silently render nothing. UPageHeroneeds a parent section: wrap inUPageSectionor a max-width container.- Form validation timing:
UFormwith Zod validates on blur by default.validate-on="input"for inline. UTableempty state: without an explicitemptyslot, an empty table shows nothing.- SSR boundary: wrap client-only animations in
ClientOnly, not entire page sections. - Component locality: a new
app/components/Foo.vueclaims app-wide ownership (global auto-import, rename blast radius, collision risk). Default to colocating as_Foo.vuenext to its page; the_prefix opts out of auto-import. Promote toapp/components/once ≥2 unrelated features consume it. Review hard-rejects single-caller globals.
Review's hard rejection list is the authoritative rubric; see /nuxt-frontend-review.
Phase 3: Polish
Refine existing interfaces against the project's own design system, not generic best practices. If $ARGUMENTS names an area (typography, motion, color), go straight to that reference.
Read the foundation files from Phase 2 if they aren't in context.
Audit Foundation Tokens
Missing tokens cause inconsistency downstream. Fix gaps before elevating.
| Token category | Where defined | What to check |
|---|---|---|
| Colors | app.config.ts colors |
primary and neutral set? Additional theme colors registered? |
| Fonts | @theme --font-* |
At least --font-sans? Display/mono if used? |
| Semantic overrides | :root / .dark --ui-* |
--ui-bg, --ui-text, --ui-border customized? |
| Component overrides | app.config.ts ui.* |
Key components themed? defaultVariants set? |
| Unnecessary tokens | @theme |
Custom tokens duplicating Tailwind builtins or --ui-*? Remove and use the existing token. |
Observe Before Changing
Polish modifies an existing voice. Read 2-3 key .vue files first and state what the interface is currently saying: copy tone, shadow and card treatment, density, hover and focus states, animation style, rhythm between sections. The goal is to match and elevate that vocabulary. Not being able to describe the existing voice in one sentence means you are not ready to polish.
Audit Consistency
Review's mechanical checks are the authoritative list (hardcoded hex/rgb, slate-/gray-/zinc-/stone- outside the configured neutral, bg-white/text-black, banned font families, custom @theme tokens duplicating --ui-*). See harlan-agent-kit:nuxt-frontend-review → references/mechanical-checks.md. Fix hits before handoff; review re-grades the same patterns.
Lead with the class-token inventory, the same command review runs:
pnpm dlx @ripast/cli css-class-scan --glob 'app/**' --sort count-desc --jsonLook for:
- Synonym clusters:
bg-gray-100+bg-slate-100+bg-zinc-100all filling one semantic role (bg-muted). Each cluster is a conformity fix. - One-off tokens (count = 1): usually a hand-tuned value that should map to a token.
- Low-count spellings near a high-count one: drift from the canonical class.
With an obvious token map, apply it via ripast css-class-rename --map tokens.json --apply (dry-run first) rather than hand-editing call sites.
Beyond the mechanical list: inconsistent shadow/radius choices across similar components (pick one per component type in app.config.ts), inline style=, raw HTML where a Nuxt UI component exists, and anything in DESIGN.md the implementation contradicts.
Audit Confirmation
Present findings before changing anything:
**Audit findings:**
Token issues (will fix):
1. Hardcoded `bg-slate-100` in HeroSection.vue:42 → should use `bg-muted`
Aesthetic observations (need your input):
2. Hero text feels small for a landing page → increase size? [intentional / fix]
3. Cards all use identical treatment → add hierarchy? [intentional / fix]- Token and consistency violations are objectively wrong per the design system: present as "will fix".
- Aesthetic judgments (sizing, hierarchy, rhythm, visual weight) need user input. Do not assume they are problems.
- Read the decisions log first:
docs/design-decisions.md, or## Design DecisionsinDESIGN.mdif the repo has not split. Anything already ruled on is settled; present only new observations. If all of them are covered, fix the token issues and move to Elevate. - Wait for the user to mark each aesthetic item "intentional" or "fix".
After confirmation, append each "intentional" choice to docs/design-decisions.md with a date. If it changes a standing rule, edit that rule in DESIGN.md in one sentence. Never add dated text to DESIGN.md. Follow the root docs budgets.
Fix only approved items.
Elevate
Once foundation and consistency are solid and the audit is confirmed. Read by symptom:
| Symptom | Reference |
|---|---|
| Feels flat/static | polish/interactions.md |
| No entrance animations | polish/motion.md |
| Typography feels generic | polish/typography.md |
| Layout cramped or boring | polish/spatial.md |
| Colors dull or inconsistent | polish/color.md |
| Not responsive enough | polish/responsive.md |
| Copy unclear or generic | polish/ux-writing.md |
| Reaching for JS/custom tokens for native CSS work | polish/css-toolkit.md |
| Chart/dashboard noisy, dishonest, or low data-ink | polish/data-viz.md |
For "make it better" or "polish everything": start with interactions.md + motion.md + color.md (highest impact), add spatial.md, typography.md, responsive.md, ux-writing.md only where the audit found issues, and consult css-toolkit.md whenever you are about to invent a token, add a JS scroll observer, or hand-code a popover, since the native CSS equivalent is usually one line.
Verify the Route Still Loads
With $DEV_PORT / $DEV_LOG allocated (see workflow reference):
curl -sf "http://localhost:$DEV_PORT/{route}" > /dev/null && echo OK || echo FAIL
grep -nE '^\s*(\[error\]|\[warn\]|ERROR|WARN|✖|✘|Hydration |Cannot find|Failed to (resolve|compile))' "$DEV_LOG" \
|| echo "dev server log clean"That is the whole self-verification at this stage. Visual and interaction verification is review's job; duplicating it here wastes context.
Polish Rules
- Keep what already works and is distinctive.
- "Same product, elevated execution", not "this is different".
- Refine rather than replace wholesale.
- Polish what's there; adding features is scope creep.
- Over-polishing kills distinctiveness. Pick 1-2 areas to elevate, leave the rest.
Polish Gotchas
- Motion-v SSR hydration:
ClientOnlywrapper oronMountedfor entrance animations - OKLCH browser support:
@supportsfallback for older Safari - Font loading flash: register fonts via Nuxt Fonts, not CSS
@import - Container queries: need
container-type: inline-sizeon the parent - Don't refactor while polishing: polish is visual refinement, not code cleanup
Polish work emits the same handoff as a build; see references/workflow.md.
Reka UI
For components Nuxt UI doesn't cover, or where it is too prescriptive, use Reka UI: unstyled, accessible Vue primitives you style with Tailwind.