Agent skill
vue
Builds UIs with Vue 3 including Composition API, reactivity, components, and state management. Use when creating Vue applications, building reactive interfaces, or working with the Vue ecosystem.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/vue-mgd34msu-goodvibes-plugin-2
SKILL.md
Vue 3
The progressive JavaScript framework for building user interfaces.
Quick Start
Create project:
npm create vue@latest my-app
cd my-app
npm install
npm run dev
Component Basics
Single-File Component (SFC)
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
function increment() {
count.value++;
}
</script>
<template>
<button @click="increment">Count: {{ count }}</button>
</template>
<style scoped>
button {
font-weight: bold;
}
</style>
Reactivity
ref - Primitive Values
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
const message = ref('Hello');
// Access with .value in script
count.value++;
message.value = 'World';
</script>
<template>
<!-- Auto-unwrapped in template -->
<p>{{ count }} - {{ message }}</p>
</template>
reactive - Objects
<script setup lang="ts">
import { reactive } from 'vue';
const state = reactive({
count: 0,
user: {
name: 'John',
email: '[email protected]',
},
});
// Direct access, no .value
state.count++;
state.user.name = 'Jane';
</script>
<template>
<p>{{ state.count }} - {{ state.user.name }}</p>
</template>
computed - Derived State
<script setup lang="ts">
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
// Writable computed
const fullNameWritable = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
const [first, last] = value.split(' ');
firstName.value = first;
lastName.value = last;
},
});
</script>
watch - Side Effects
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
const message = ref('');
// Watch single source
watch(count, (newVal, oldVal) => {
console.log(`Count: ${oldVal} -> ${newVal}`);
});
// Watch multiple sources
watch([count, message], ([newCount, newMsg], [oldCount, oldMsg]) => {
console.log('Changed');
});
// Watch with options
watch(
count,
(newVal) => console.log(newVal),
{ immediate: true, deep: true }
);
// Auto-track dependencies
watchEffect(() => {
console.log(`Count is ${count.value}`);
});
</script>
Props
<!-- ChildComponent.vue -->
<script setup lang="ts">
interface Props {
title: string;
count?: number;
items?: string[];
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => [],
});
</script>
<template>
<h1>{{ props.title }}</h1>
<p>Count: {{ props.count }}</p>
</template>
<!-- Parent -->
<ChildComponent title="Hello" :count="5" :items="['a', 'b']" />
Events
<!-- ChildComponent.vue -->
<script setup lang="ts">
const emit = defineEmits<{
(e: 'update', value: number): void;
(e: 'delete', id: string): void;
}>();
function handleClick() {
emit('update', 42);
}
</script>
<template>
<button @click="handleClick">Update</button>
<button @click="emit('delete', '123')">Delete</button>
</template>
<!-- Parent -->
<ChildComponent @update="handleUpdate" @delete="handleDelete" />
v-model
Basic v-model
<!-- CustomInput.vue -->
<script setup lang="ts">
const model = defineModel<string>();
</script>
<template>
<input v-model="model" />
</template>
<!-- Parent -->
<CustomInput v-model="message" />
Named v-model
<!-- UserForm.vue -->
<script setup lang="ts">
const firstName = defineModel<string>('firstName');
const lastName = defineModel<string>('lastName');
</script>
<template>
<input v-model="firstName" placeholder="First" />
<input v-model="lastName" placeholder="Last" />
</template>
<!-- Parent -->
<UserForm v-model:firstName="first" v-model:lastName="last" />
Slots
Default Slot
<!-- Card.vue -->
<template>
<div class="card">
<slot>Default content</slot>
</div>
</template>
<Card>
<p>Custom content</p>
</Card>
Named Slots
<!-- Layout.vue -->
<template>
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</template>
<Layout>
<template #header>
<h1>Title</h1>
</template>
<p>Main content</p>
<template #footer>
<p>Footer</p>
</template>
</Layout>
Scoped Slots
<!-- List.vue -->
<script setup lang="ts">
defineProps<{ items: string[] }>();
</script>
<template>
<ul>
<li v-for="(item, index) in items" :key="index">
<slot :item="item" :index="index" />
</li>
</ul>
</template>
<List :items="['a', 'b', 'c']">
<template #default="{ item, index }">
{{ index }}: {{ item }}
</template>
</List>
Lifecycle Hooks
<script setup lang="ts">
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount,
} from 'vue';
onBeforeMount(() => {
console.log('Before mount');
});
onMounted(() => {
console.log('Mounted - DOM available');
});
onBeforeUpdate(() => {
console.log('Before update');
});
onUpdated(() => {
console.log('Updated');
});
onBeforeUnmount(() => {
console.log('Before unmount');
});
onUnmounted(() => {
console.log('Unmounted - cleanup');
});
</script>
Template Refs
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const inputRef = ref<HTMLInputElement | null>(null);
onMounted(() => {
inputRef.value?.focus();
});
</script>
<template>
<input ref="inputRef" />
</template>
Provide / Inject
<!-- Parent.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue';
const theme = ref('dark');
const updateTheme = (newTheme: string) => {
theme.value = newTheme;
};
provide('theme', { theme, updateTheme });
</script>
<!-- DeepChild.vue -->
<script setup lang="ts">
import { inject } from 'vue';
const { theme, updateTheme } = inject('theme', {
theme: ref('light'),
updateTheme: () => {},
});
</script>
<template>
<p>Theme: {{ theme }}</p>
<button @click="updateTheme('light')">Light</button>
</template>
Composables
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(event: MouseEvent) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => {
window.addEventListener('mousemove', update);
});
onUnmounted(() => {
window.removeEventListener('mousemove', update);
});
return { x, y };
}
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse';
const { x, y } = useMouse();
</script>
<template>
<p>Mouse: {{ x }}, {{ y }}</p>
</template>
Async Composable
// composables/useFetch.ts
import { ref, watchEffect, toValue, type Ref } from 'vue';
export function useFetch<T>(url: Ref<string> | string) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(true);
watchEffect(async () => {
data.value = null;
error.value = null;
loading.value = true;
try {
const response = await fetch(toValue(url));
data.value = await response.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
});
return { data, error, loading };
}
Directives
Built-in Directives
<template>
<!-- Conditional -->
<p v-if="show">Visible</p>
<p v-else-if="other">Other</p>
<p v-else>Hidden</p>
<p v-show="show">Toggle visibility</p>
<!-- Loop -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- Binding -->
<img :src="imageUrl" :alt="imageAlt" />
<div :class="{ active: isActive, disabled }" />
<div :style="{ color: textColor, fontSize: '16px' }" />
<!-- Events -->
<button @click="handleClick">Click</button>
<input @keyup.enter="submit" />
<form @submit.prevent="handleSubmit" />
<!-- Two-way binding -->
<input v-model="text" />
<input v-model.trim="text" />
<input v-model.number="age" type="number" />
</template>
Custom Directive
// directives/vFocus.ts
import type { Directive } from 'vue';
export const vFocus: Directive = {
mounted(el) {
el.focus();
},
};
<script setup lang="ts">
import { vFocus } from '@/directives/vFocus';
</script>
<template>
<input v-focus />
</template>
Transition
<template>
<Transition name="fade">
<p v-if="show">Hello</p>
</Transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
Best Practices
- Use Composition API - Better TypeScript support, logic reuse
- Extract composables - Reusable stateful logic
- Type props and emits - Full type safety
- Use defineModel - Simpler v-model implementation
- Prefer ref over reactive - More explicit, fewer gotchas
Common Mistakes
| Mistake | Fix |
|---|---|
| Forgetting .value | Use .value in script, auto-unwrap in template |
| Destructuring reactive | Use toRefs() to keep reactivity |
| Wrong ref type | ref for primitives, reactive for objects |
| Missing key in v-for | Always provide unique :key |
| Mutating props | Emit events to parent |
Reference Files
- references/composition-api.md - Full API reference
- references/reactivity.md - Reactivity deep dive
- references/typescript.md - TypeScript patterns
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?