Vue Conventions
SkillFiles & storageApply when writing or reviewing .vue files, or rendering a date or time. Esposter's Vue 3 SFC conventions, how a <script setup> and its template are ordered and bound; single-use handlers inlined, v-model over split bindings, never normalizeString in Vue, defineProps on a local Props interface, useTemplateRef, a computed earned by reuse, work or identity, the watch decision tree, browser globals behind checkIsServer, and every rendered date a NuxtTime.
Use Vue Conventions in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add Vue Conventions and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use the Vue Conventions skill
Details
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; Ahel provides instructions and does not run this skill.
No other account needed.
Add Ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
What this skill tells your AI
The instructions your AI receives, as published by esposter/esposter in .agents/skills/vue/SKILL.md and read by Ahel’s review.
<script setup lang="ts">at the top of every SFC. Prefer attributify over<style>blocks; when a block is genuinely needed, thestylingskill owns its rules (scoped,lang="scss", library CSS).- Self-closing tags for empty components/elements:
<Component />. - Blank-line placement (templates, consts, returns, blocks) and comment attachment — the
formattingskill ("Blank Lines",references/comment-placement.md). - Links,
:to,navigateTo, reactive route reads, route validation, page keys and route-synced tabs — see theroutingskill.
Settled — do not re-propose
- A lint rule for computed extraction — oxlint cannot resolve a template identifier back to its script declaration (the SFC's halves are separate ASTs to it), and neither half of the rule is syntactic, since "walks a collection" and "is bound to a prop" both need the other side of the boundary.
Ordering — references/ordering.md
Macros defineSlots → defineModel → defineProps → defineEmits, then every const, defineExpose last; declarations run page metadata → macros → framework value composables → Pinia stores → everything else; attributes run v-model/v-for → class → attributify → valued props (alphabetical) → bare booleans → @event. The exceptions to each, and the typing defineModel and defineSlots take, are that page.
Single-use functions must be inlined — references/inline-handlers.md
Read it when naming, extracting or reviewing a function used once. A single-use function that only defers a block must be inlined; the discriminator is whether its name states its trigger (onMount, handleX — inline it, including template handlers however long) or its result (getFooType — keep it, single use is fine). The page owns every form of ceremony, the template-scope exception, and the list of legitimate keeps.
- Prefer
useEventListenerover manualaddEventListener/removeEventListener— it auto-removes on unmount, so the handler can be inlined; the target and SSR rules are on that page. - Never destructure event parameters (
no-restricted-syntax, for a DOM*Eventannotation or a listener API's callback) — destructured methods (preventDefault, …) lose theirthisand throw "Illegal invocation". A template@eventarrow is out of the selector's reach, since a component's payload is not an event: keep the fulleventthere by reading.
v-model, Inputs and Forms — references/forms.md
Read it when an input needs the split :model-value + @update:model-value form, or when a form handles both create and edit (the isCreate prop, a single values ref).
- Prefer
v-model="ref"over the split form whenever the update is a direct assignment to a single ref. - Never trim in Vue —
restrictedTrimSyntaxes.jsbansnormalizeStringand.trim()in script and template, since tRPC input schemas already normalize and a string a component parses belongs in a service. Trust the server schema — validity is asafeParseof the shared schema driving:disabled, and submit handlers pass raw values with no guards. Both rules in full, including what dirty-state comparison parses, are on that page.
Template Conventions
- Truthiness —
v-if="value", notv-if="value !== null". Explicit null/undefined comparisons only when distinguishing falsy values (0valid,falsemeaningful,nullvsundefinedmatters). - No bare function references in
@eventbindings (vue/no-restricted-syntax) — a bare ref forwards the event object as first arg (almost always unintended). Usefn()for zero-arg calls, an arrow function naming the payload when it is wanted:@select="(id) => selectRole(id)". v-fordestructures what it reads, keeping the whole item only when it is passed on (references/template-bindings.md).v-bindshorthand is autofixed byvue/v-bind-style.- Never use
.valuein templates — Vue auto-unwraps refs, soref.valuereads.valueon the unwrapped object (usuallyundefined). Writefn(ref);.valueis for<script setup>only. - No allocating expressions in render positions —
Object.*in a:prop,v-forsource or{{ }}allocates a fresh reference every render. Enforced byvue/no-restricted-syntax, whose message states the fix. - Event modifiers over raw event methods (
vue/no-restricted-syntax);stopImmediatePropagation()is banned (references/template-bindings.md). - Reassigning a
defineModelvs mutating it in place is a deliberate semantic choice — don't "fix" one into the other. references/template-gotchas.md— read it when a directive or slot renders nothing, or vue-tsc cannot see a template identifier:v-htmlon a component, a dotted slot name, a guard that stops narrowing at a closure inside an inline handler, and why a type-only import is enough for a template cast.
Props, Refs & Computed
- Props typed from a third-party component carry
// @TODO: https://github.com/vuejs/core/issues/11371, one per site (references/props.md). definePropstakes a locally declaredinterface Props, never exported (props-interfaceplugin,references/props.md).- A local bound to a prop is named after it, so the
:propshorthand works; a module-scope constant is not a local (references/props.md). - Optional refs omit the initial value —
ref<T>(), neverref<T | undefined>(undefined); both that and a string ref without its""areno-restricted-syntaxerrors. - Template refs use
useTemplateRefwith no generic and noRefsuffix (template-ref/require-ref-name), and a Three object the script moves is created there and mounted with<primitive>rather than reached through one (references/template-refs.md). - Sort at display time, in the computed feeding the template, never in the store (
references/props.md). - Computed by cost and identity, never by use count —
references/computed-extraction.md— read it before extracting or inlining anycomputed. A computed is a cache with a price, so it earns its place on reuse (binds to 2+ props), work (parses, formats, filters, maps, sorts, reduces, walks a collection), or identity (allocates an object/array/function bound to a prop). Everything else inlines: comparisons, booleans, ternaries, template literals, property reads, arithmetic, map lookups. The page owns the traps, the keeps that override cost, and the cadence question — work run per event that only changes per boundary. - Map lookups over computed — when a value depends on an enum/discriminant key, use
Map[type]directly in the template (Map[type].valuefor multiple properties). Fall back to a computed only when the lookup is duplicated in 2+ places. - Writable computed over
watch+ local ref (references/watch-decision-tree.md, case 1).
Reading the auth session — references/auth-session.md
Read it when anything needs the signed-in user. Two call forms, and the access shape follows the form: await authClient.useSession(useFetch) in async SSR-relevant context (session.value?.user.id), the bare authClient.useSession() wherever you can't await (session.value.data?.user.id).
When (not) to watch — references/watch-decision-tree.md
Read it before writing any watch, or when a local ref mirrors a prop/store value. In short: a read-only derived value is a computed; form state initialized from a prop/store initializes the ref directly (watchImmediate to set an initial value is always a smell) and resyncs via useCloned; an id the instance is keyed by cannot change, so read it once in onMounted. Watching is correct for bridging imperative APIs (Phaser, Tiptap, Desmos) and for async side effects of state that genuinely varies under a live instance.
watchDeep/watchImmediatereplace the option object onwatch(no-restricted-syntax), anddeepskips Vue's own changed check. Why the rule reaches.ts, where the aliases do not exist, how both flags compose, and when a deep watcher's re-run is the bug: that page ("Writing the watch").- Never
watchEffect(no-restricted-syntax, itsPost/Synctwins included) — alwayswatchwith explicit dependencies; implicit tracking is hard to audit and re-runs on unrelated changes. Wrap a prop dependency in a getter:watch(() => isActive, ...).
Lifecycle hooks and the browser — references/lifecycle-and-ssr.md
A browser global read at setup scope runs on the server too, and the failure is a render that never reaches the client. Placing a lifecycle hook, or touching window, document or anything else the server does not have, is that page.
The Options API runtime is compiled out — references/options-api.md
Ours are <script setup> only (vue/component-api-style, which bans plain defineComponent(…) with it). vue.optionsApi is off and stays off, so a dependency shipping Options API .vue components mounts and then dereferences off undefined with nothing thrown to name the cause. Every Vitest worker runs the same Vue, so a component test mounting one fails as the app does. Read the page before adding one, or when a node_modules component renders blank.
Dates Are <NuxtTime> — references/dates.md
Every rendered date is a <NuxtTime>; formatDate(…), toLocaleDateString(), useTimeAgo and useDateFormat inside a .vue are vue/no-restricted-syntax errors, and a hand-written <time> is a vue/no-restricted-html-elements one. Standard: apps/web/content/docs/architecture/date-time-display.md. Read the page for what the lint rule can't say — options rather than format strings, what bare title really renders, and the client-rendered message list as the one exception.
Reference pages
references/template-bindings.md— when writing av-for, av-bindor an event modifier.references/props.md— when declaring props, binding a local to a prop, or typing props from a third-party component.references/template-refs.md— when taking a ref to an element, a child component or a Tres element.references/ordering.md— when laying out a<script setup>block's macros and declarations, or an element's attributes.references/inline-handlers.md— when naming, extracting or reviewing a function used once.references/forms.md— when an input needs the split binding, or a form handles both create and edit.references/template-gotchas.md— when a directive or slot renders nothing, or vue-tsc cannot see a template identifier.references/computed-extraction.md— before extracting or inlining anycomputed.references/auth-session.md— when anything needs the signed-in user.references/watch-decision-tree.md— before writing anywatch, or when a localrefmirrors a prop or store value.references/lifecycle-and-ssr.md— when placing a lifecycle hook, or touching a browser global.references/options-api.md— before adding a dependency that ships.vuecomponents, or when one renders blank.references/dates.md— when rendering a date or time in a.vuefile.
Signals
- GitHub stars
- 23
- Forks
- 3
- Last commit
- Oct 2026
Advanced
- Item type
- skill
- Key
vue-esposter- Source
- github.com/esposter/esposter
Related picks
Skill · yamcodes
The pick for Nuxterror-tracking-nuxt
Skill · posthog
The pick for Nuxtsw-vue-part
Skill · onweekendd
The pick for Vuevue-composable-patterns
Skill · esposter
The pick for Vuesetup-ts-deep-modules
Skill · mattpocock
The pick for TypeScripttypescript-pro
Skill · jeffallan
The pick for TypeScript