Vue Conventions — Framework Skill
Vue-specific rules for modern Vue 3: single-file components with <script setup>, the Composition
API, composables, Pinia, and SSR with Nuxt. It gives the Vue form of rules that core-typescript
and architecture-and-design set in general terms.
Builds on.
core-typescript(language rules) andarchitecture-and-design(design), plusaccessibilityfor UI work. Load a sibling only when the task turns on its layer; if it is not loaded, apply that layer from general knowledge and do not block.
This SKILL.md is self-sufficient: the Ruleset below is the complete, enforceable list. Each
references/<topic>.md holds that group's reasoning and ❌ / ✅ code, and
references/worked-example.md a full review pass; open them for depth when your runtime allows.
How to Use This Skill
Pick the mode that matches the task. Do the steps in order.
| Mode | Steps |
|---|---|
| Generate — write a new component or composable | 1. <script setup lang="ts">, typed defineProps / defineEmits, defineModel for two-way (components). 2. ref for state, computed for derived; do not destructure a reactive (reactivity). 3. Pull shared stateful logic into a useX composable that returns refs (composables). 4. Key every v-for on a stable id (templates). 5. Run the Ruleset as a checklist. Fix each fail before you hand off. |
| Review — check a pull request or a diff | 1. Run the Ruleset against the diff. 2. Write one finding per fail, in the Output Format below. 3. Order the findings: must-fix first, then consider. 4. If nothing fails, say so in one line. Do not invent findings. |
| Migrate — Options API or Vue 2 to modern Vue 3 | 1. Move one component at a time to <script setup>; data → ref, computed stays, methods → functions, watch → watch/watchEffect. 2. Extract mixins into composables. 3. Replace the event bus and Vuex with provide/inject or Pinia (state). 4. One component per commit; keep the tests green. |
Output Format
Write one finding per line:
<severity> · <topic> · <file>:<line> — <what is wrong>. <the fix as an action>.
<severity>ismust-fix(breaks a rule in this skill, the compiler, or a lint rule) orconsider(safe, but a rule prefers another form).<topic>is a Ruleset topic slug (components,reactivity,composables,templates,state,forms,rendering,server,testing).
Rules for Every Mode
- Name the Ruleset topic when you enforce a rule.
- Prefer the current API:
<script setup>overdefineComponent,defineModelover a manualmodelValueprop plusupdate:modelValueevent, Composition API over Options API in new code. - Consistency within a file wins. When a file is entirely Options API, match it and note the gap rather than half-converting it.
Ruleset
components → references/components.md
-
<script setup lang="ts">for a new component; no Options API and noexport default defineComponent({ ... })with an options object. -
definePropsanddefineEmitsare typed with a type argument, not a runtime object;defineModelfor two-way binding instead of amodelValueprop plus anupdate:modelValueemit. - Prop defaults use reactive props destructure (
const { size = 'md' } = defineProps<Props>(), Vue 3.5), notwithDefaults. - One component per
.vuefile,PascalCasename matching the file; anameis set (or inferred) for the devtools and<KeepAlive>. -
defineOptionsfor component options that are not props (e.g.inheritAttrs: false), not a second<script>block where avoidable. - Caller-supplied content comes through named and scoped slots; the slot-vs-prop decision is
component-api-design, slots-vs-config. -
<style scoped>(or CSS Modules); a child-piercing rule uses:deep()deliberately, never an unscoped global leak from a component file.
reactivity → references/reactivity.md
-
refis the default for state;reactiveonly for a genuinely object-shaped local group, and it is never destructured or reassigned (that drops reactivity — usetoRefs/toRef). -
.valuein script; no.valueon a top-level ref in a template. -
computedis pure — no side effect, no async, no mutation of another ref inside it. -
watchlists its source explicitly and does the minimum;watchEffectonly when the dependencies are truly dynamic; neither is used to derive a value thatcomputedcan (state). -
shallowRef/shallowReactivefor a large or externally-owned structure; a deepwatchis a deliberate, commented choice. - Exposed reactive state that callers must not mutate is wrapped in
readonly().
composables → references/composables.md
- Shared stateful logic is a
useX()composable in its own file that returns refs / computeds (and functions), not a mixin and not a renderless component. - A composable that takes reactive input accepts a ref or a getter and reads it with
toValue(). - Lifecycle hooks and
watchinside a composable are registered synchronously at call time (noawaitbefore them). - The composable has no module-scope mutable state unless it is a deliberate singleton — that state is shared across every caller and leaks across requests
in SSR (
server). - It returns a plain object of named values, not a single
reactivebag.
templates → references/templates.md
- Every
v-forhas a:keybound to a stable domain id — never the array index for a list that can reorder, grow, or shrink. -
v-ifandv-forare never on the same element; thev-ifmoves to a<template>wrapper or into acomputedfiltered list. - No non-trivial expression in a binding — anything past a property read or one call goes into a
computed. -
v-htmlis used only on sanitized or trusted content (architecture-and-design, security). - Element choice and accessible names follow
accessibility;useId()supplies a stable label /aria-*id across server and client. -
v-once/v-memoappear only on a list row or subtree measured to be a render cost, not by default.
state → references/state.md
- State is local (
refin the component) until a second component needs it; thenprovide/injectwith a typedInjectionKey, and only then a store. - Pinia is the store for cross-view client state, sized against the
architecture-and-designstate tiers — reach for it when a composable plusprovidewould not scale, not by default. - A Pinia store is defined with the setup syntax and is written through its actions (or one
$patch); a component never assigns store state directly or reassignsstore.$state. - Server data (fetch, cache, revalidate) is not held in Pinia or a
ref— it uses a query cache (TanStack Query, or NuxtuseAsyncData/useFetch) keyed by its inputs (architecture-and-design, state-and-data). - URL-owned state — filters, tab, pagination, page — lives in the route query, not a store (
architecture-and-design, state-and-data).
forms → references/forms.md
-
v-model(with.lazy/.number/.trimwhere they fit) ordefineModelfor a custom field component; a native input is not needlessly wrapped in reactive plumbing. - Validation runs off the same schema the server validates with (VeeValidate + a schema, or a resolver), and the server re-validates
(
architecture-and-design, forms). - Field error state and messages are derived (
computed) from the validation result, not copied into separate refs that can drift. - Entered values survive a failed submit; each error maps back to its field; the submit button is not the only feedback for a blocked save.
- A field the current step does not use is removed from the payload or explicitly disabled, not just hidden with
v-if.
rendering → references/rendering.md
- A component with a top-level
awaitinsetupis rendered inside<Suspense>with a fallback and an error boundary (onErrorCapturedor a wrapper). - Route components and heavy below-the-fold components are code-split with
defineAsyncComponent/ the router's lazy import. -
<KeepAlive>is scoped to a small:includelist, not wrapped around a whole router view by default. - A list past a few hundred rows is virtualized; no object / array / function literal is created in the template as a child prop — it is hoisted or
a
computed. - A deep
watchover a large structure, and awatchEffectthat re-runs too often, are replaced with a targetedwatchon the specific field.
server → references/server.md
- No
window/document/localStorageat the top level ofsetup; it goes inonMountedor behindimport.meta.client(Nuxt) /!import.meta.env.SSR(Vite SSR). - SSR-shared state uses the framework primitive (
useStatein Nuxt), never a module-scoperef. -
useAsyncData/useFetchhave an explicit, stable key and run the fetch once across server and client, not again on hydration. - A hydration mismatch is fixed at its cause (non-deterministic render,
Date.now(), random, browser-only branch), not silenced; genuinely client-only UI is wrapped in<ClientOnly>. - Server-only modules (secrets, a DB client,
server/code) are never imported into a component that ships to the client.
testing → references/testing.md
- Components are mounted with
@testing-library/vue(or@vue/test-utilsmount, notshallowMount) and queried by role and accessible name, never by component internals or a CSS selector onwrapper. - Interaction is driven by
@testing-library/user-event(awaited), and assertions are on rendered output — not onemitted()call counts orvmstate as a stand-in for behavior. - No stubbed
fetchor mocked composable stands in for data; the boundary rule (MSW) istest-quality, test-doubles. - Async updates are awaited (
await nextTick()/flushPromises()/findBy*) before the assertion. - A composable is tested through a host component that uses it; mounting-to-test in isolation only when there is no component.
- Teleported content (modal, tooltip) is queried through
screen/document, not the mounted wrapper. - Each test also passes the
test-qualityRuleset — asserts on rendered behavior not internals, has a meaningful assertion, is deterministic. This group is the Vue mechanics;test-qualityjudges the test itself.
Limits
This skill is Vue framework rules. It does not cover:
- Language rules (see
core-typescript) or framework-neutral architecture (seearchitecture-and-design). - A meta-framework's routing, data layer, and deployment specifics beyond the SSR boundary here — Nuxt modules, Nitro, route rules,
server/API design. - The Options API in depth — new code uses
<script setup>; for a legacy Options API app, migrate first (the Migrate mode above). - Vuex (superseded by Pinia), and store internals beyond the state tiers in
architecture-and-design. - Vue 2, the pre-
<script setup>Composition APIsetup()return style, and animation (<Transition>choreography). - Styling is
styling-and-design-tokens; i18n (vue-i18npolicy) isi18n-and-localization; loading and interaction cost isweb-performance. - Accessibility depth — semantic elements and names are noted where they fit; the full lens is
accessibility.
This skill decides the Vue API. It does not replace reading the component and understanding the domain.
References
This skill composes with:
core-typescript— the language base; SFCs and<script setup>macros do not exempt code from it.architecture-and-design— the design layer. On a conflict it decides the design, this skill decides the Vue API.accessibility— the review lens for UI; Vue's tools are semantic templates,useId(), and primitive libraries (Reka UI, formerly Radix Vue, and Headless UI).test-quality— judges the individual test this skill'stestinggroup produces.react/angular— the sibling framework skills.