This skill enforces Vue 3.5+ single-file component conventions. Composition API + <script setup> only; type-safe declarations; modern macros (defineModel / useTemplateRef / useId).
Apply only when the project uses vue ^3.5 or higher. If package.json pins vue ^3.4 or earlier, STOP and ask the user — 3.5+ APIs (useId, useTemplateRef, reactive props destructure, onWatcherCleanup) won't work. Do not write 2.x compatibility code.
Core principles
<script setup>only, no Options API (data()/methods/computed: {}/watch: {}/mounted/mixins).- TypeScript type declarations for props/emits, not runtime
defineProps([...])array orPropTypeimports. defineModel()forv-model, not manualprops/emitspairs.useTemplateRef()for template refs (Vue 3.5+), not the legacy static-ref binding.useId()for stable IDs in form/a11y, notMath.random()/ module-level counters.- Reactive props destructure (Vue 3.5+) over
withDefaults()/props.foo. - Composables for shared logic, not mixins.
Required form
<script setup lang="ts">
import { computed, useTemplateRef, useId } from 'vue'
// Props (type declaration + reactive destructure with defaults)
const { items, selected = [] } = defineProps<{
items: Item[]
selected?: string[]
}>()
// Emits (type declaration)
const emit = defineEmits<{
change: [id: string]
delete: [id: string]
}>()
// v-model (3.4+)
const modelValue = defineModel<string>()
// Slots type hints
const slots = defineSlots<{
default(props: { item: Item }): any
header(props: { count: number }): any
}>()
// Template ref (3.5+)
const inputRef = useTemplateRef<HTMLInputElement>('input')
// Stable IDs (3.5+)
const id = useId()
</script>
<template>
<input :id ref="input" />
</template>
Forbidden patterns
Component definition
- Options API:
export default { data() / methods / computed / watch / mounted / mixins }— use<script setup>. defineComponent({ ... })— only legitimate inside non-SFC.ts/.jsfiles for type inference; never inside SFC.- Plain
<script>(withoutsetup) for things expressible in<script setup>(props, emits, options). - Mixins (
mixins: [...]) — extract into a composable (useFoo()). - Global filters / global directives registered via
app.directivefor one-off use — define locally asvMyDirectivein<script setup>.
Props / Emits
- Runtime array form
defineProps(['foo', 'bar'])— use type declaration. PropType<T>imports — type declaration replaces it.withDefaults(defineProps<T>(), { ... })when project is on Vue 3.5+ — use reactive destructure with native defaults:const { foo = 'x' } = defineProps<T>().- Untyped emits:
defineEmits(['change'])— type declaration:defineEmits<{ change: [id: string] }>(). - Manual
props.modelValue+emit('update:modelValue', ...)for v-model — usedefineModel().
Refs / state
- Static template ref name binding (
const foo = ref(); ... <div ref="foo">) when on 3.5+ — useuseTemplateRef('foo'). Math.random()/Date.now()/ module-levellet id = 0for component IDs — useuseId().reactive()for primitive-heavy state whereref()reads cleaner — both are valid; preferref()unless you have a deeply nested object.this.$refs/this.$emitsyntax — that's Options API.
Watchers / effects
- Side effects in
mounted/beforeMountOptions hooks — useonMounted()/watchEffect(). - Async cleanup logic via flag tracking — use
onWatcherCleanup()(3.5+). - Hand-written composable (
useXxx) when the project already exposes one undercomposables/(or auto-imported via Nuxt). grepcomposables/andapp/composables/first; reuse if found.
Component decomposition
Single-file components grow brittle past a threshold. When a .vue file approaches 400 lines, evaluate splitting:
- Extract reusable UI subtree → child
.vuewith its own props/emits. - Extract shared state/logic (data fetch, form state, derived values) → composable (
useXxx()incomposables/or~/composables/). - Extract pure helper functions (no reactivity) → plain
.tsutility module.
When approaching 800 lines, splitting is mandatory before adding more logic. Report and ask the user which slice to extract first if it's not obvious.
Imports
Nuxt projects auto-import ref / computed / watch / useFetch / etc. — do not add explicit imports for auto-imported APIs. Plain Vue (Vite) projects must import from 'vue' explicitly.
When you can't follow these rules
If the codebase has Options API legacy components and you must edit them, STOP and report:
File [path] uses Options API. Approve one of: (A) leave as-is, change minimally in Options style; (B) refactor to
<script setup>first (provide before/after for the relevant section); (C) extract the change to a child component using<script setup>.
Do not silently mix Options-style patches into a <script setup> file or vice versa.
Verification (grep after every .vue change)
# Options API leftovers
grep -rE 'export default \{' --include='*.vue' . # → migrate to <script setup>
grep -rE 'data\(\)\s*\{' --include='*.vue' . # Options data
grep -rE 'methods:\s*\{' --include='*.vue' . # Options methods
grep -rE 'mixins:\s*\[' --include='*.vue' . # replace with composable
grep -rE 'PropType' --include='*.vue' . # replace with type declaration
# legacy v-model pattern
grep -rE "emit\('update:" --include='*.vue' . # check if it can move to defineModel
# string ref
grep -rE 'ref="[a-zA-Z]+"' --include='*.vue' . | grep -v useTemplateRef # 3.5+ should use useTemplateRef
# legacy props pattern
grep -rE 'defineProps\(\[' --include='*.vue' . # array form — switch to type declaration
grep -rE 'withDefaults\(' --include='*.vue' . # 3.5+ should use destructuring defaults
# SFC over 400 lines
find . -name '*.vue' -exec wc -l {} \; | awk '$1>400 {print}'
Full API: https://vuejs.org/api/sfc-script-setup
Source: zhaojiannet/claude-skills — distributed by TomeVault.