A guide for developing with Vue.js 3, focusing on the Composition API and single-file components.
Install
mkdir -p .claude/skills/vue-terminalskills && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/19526" && unzip -o skill.zip -d .claude/skills/vue-terminalskills && rm skill.zipInstalls to .claude/skills/vue-terminalskills
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Vue.js is a progressive JavaScript framework for building user interfaces. It features the Composition API with reactive refs and computed values, single-file components, and an approachable learning curve with powerful scaling capabilities.Key capabilities
- →Implement reactive state using Composition API
- →Structure applications with Pinia stores
- →Define reusable logic with composables
- →Configure routing with Vue Router
- →Create type-safe components with script setup
How it works
Vue 3 utilizes the Composition API for reactive state management and single-file components. It use a virtual DOM with compiler optimizations to scale from simple prototypes to complex applications.
Inputs & outputs
When to use vue
- →Create a new Vue 3 project with Vite
- →Implement reactive state using Composition API
- →Structure a Vue application with Pinia
- →Build reusable components
About this skill
Vue
Overview
Vue 3 builds interfaces from single-file components (.vue) that hold a template, a script setup block and styles. State is reactive (ref, computed, watch), shared state lives in Pinia, navigation in Vue Router, and reusable logic in composables. Versions checked in October 2026: vue 3.5.44 (3.6 with Vapor Mode is still a release candidate, so do not use it unless asked), vue-router 5.4, pinia 4.1, vite 8, create-vue 3.24. Router 5 and Pinia 4 keep the Router 4 and Pinia 2 style APIs used below.
Instructions
Create a project
npm create vue@latest -- --ts --router --pinia --vitest --eslint taskboard
cd taskboard && npm install && npm run dev
The flags skip the interactive prompts; run npm create vue@latest -- --help for the list (--bare omits the example code). create-vue itself needs Node 22.18+ or 24.12+. npm run build runs vue-tsc type-checking before the Vite build, so type errors fail the build.
Project layout
src/
main.ts entry: createApp, plugins
App.vue root component with <RouterView />
router/index.ts routes and guards
stores/ Pinia stores (articles.ts, auth.ts)
composables/ use* functions (useApi.ts)
components/ shared components
views/ route-level components
types/article.ts shared TypeScript types
// src/types/article.ts
export interface Article {
id: number
slug: string
title: string
excerpt: string
published: boolean
}
Components: props, emits, v-model
Use script setup. Since Vue 3.5 props can be destructured and stay reactive; defineEmits takes named tuples; defineModel (3.4+) replaces the modelValue prop plus update:modelValue emit.
<!-- src/components/ArticleCard.vue -->
<script setup lang="ts">
import type { Article } from '@/types/article'
const { article, showDelete = false } = defineProps<{
article: Article
showDelete?: boolean
}>()
const emit = defineEmits<{ delete: [id: number] }>()
</script>
<template>
<article class="card">
<RouterLink :to="`/articles/${article.slug}`">
<h2>{{ article.title }}</h2>
</RouterLink>
<p>{{ article.excerpt }}</p>
<button v-if="showDelete" @click="emit('delete', article.id)">Delete</button>
</article>
</template>
<!-- src/components/SearchBox.vue -->
<script setup lang="ts">
const query = defineModel<string>({ default: '' })
</script>
<template>
<input v-model="query" type="search" placeholder="Search articles" />
</template>
Reactivity
const count = ref(0) // read and write count.value in script
const doubled = computed(() => count.value * 2) // cached, recomputed when count changes
watch(count, (newVal, oldVal) => console.log(`Count changed from ${oldVal} to ${newVal}`))
Pinia store (setup syntax)
// src/stores/articles.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Article } from '@/types/article'
export const useArticlesStore = defineStore('articles', () => {
const articles = ref<Article[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const published = computed(() => articles.value.filter((a) => a.published))
async function fetchAll() {
loading.value = true
error.value = null
try {
const res = await fetch('/api/articles')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
articles.value = await res.json()
} catch (e) {
error.value = e instanceof Error ? e.message : String(e)
} finally {
loading.value = false
}
}
async function create(data: Pick<Article, 'title' | 'excerpt'>) {
const res = await fetch('/api/articles', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
articles.value.unshift(await res.json())
}
return { articles, loading, error, published, fetchAll, create }
})
In a component, destructure state and getters with storeToRefs(store); take actions straight from the store. Destructuring state directly loses reactivity.
<!-- src/views/ArticlesView.vue -->
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import ArticleCard from '@/components/ArticleCard.vue'
import SearchBox from '@/components/SearchBox.vue'
import { useArticlesStore } from '@/stores/articles'
const store = useArticlesStore()
const { published, loading, error } = storeToRefs(store)
const query = ref('')
const visible = computed(() =>
published.value.filter((a) => a.title.toLowerCase().includes(query.value.toLowerCase())),
)
onMounted(store.fetchAll)
</script>
<template>
<div>
<h1>Articles</h1>
<SearchBox v-model="query" />
<p v-if="loading">Loading...</p>
<p v-else-if="error">Could not load articles: {{ error }}</p>
<div v-else class="cards">
<ArticleCard v-for="article in visible" :key="article.id" :article="article" />
</div>
</div>
</template>
Composable
// src/composables/useApi.ts
import { ref, type Ref } from 'vue'
export function useApi<T>(url: string) {
const data: Ref<T | null> = ref(null)
const error = ref<string | null>(null)
const loading = ref(false)
async function execute() {
loading.value = true
error.value = null
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
data.value = (await res.json()) as T
} catch (e) {
error.value = e instanceof Error ? e.message : String(e)
} finally {
loading.value = false
}
}
return { data, error, loading, execute }
}
Router with an auth guard
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/', component: () => import('@/views/HomeView.vue') },
{ path: '/articles', component: () => import('@/views/ArticlesView.vue') },
{ path: '/articles/:slug', component: () => import('@/views/ArticleView.vue'), props: true },
{ path: '/login', component: () => import('@/views/LoginView.vue') },
{ path: '/admin', component: () => import('@/views/AdminView.vue'), meta: { requiresAuth: true } },
],
})
router.beforeEach((to) => {
const auth = useAuthStore() // safe: guards run after app.use(createPinia())
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return { path: '/login', query: { redirect: to.fullPath } }
}
})
export default router
auth is a small Pinia store exposing token and an isAuthenticated computed. With props: true, a view receives :slug as a prop (const { slug } = defineProps<{ slug: string }>()).
Entry point and typed provide/inject
// src/keys.ts
import type { InjectionKey } from 'vue'
export const apiBaseKey: InjectionKey<string> = Symbol('apiBase')
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { apiBaseKey } from './keys'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.provide(apiBaseKey, '/api')
app.mount('#app')
// in any component: const apiBase = inject(apiBaseKey, '/api')
Examples
Example 1: Searchable article list
User: "Add a page that lists published articles from /api/articles with a search box."
Create ArticlesView.vue, ArticleCard.vue, SearchBox.vue and the articles store as above, and register /articles in the router. Run npm run build: it type-checks every .vue file and fails on a wrong prop or emit. In the browser the page shows "Loading...", then the cards, and typing in the box filters by title.
Example 2: Protect the admin area
User: "Only logged-in users may open /admin; send others to /login and come back after."
Add meta: { requiresAuth: true } to the route, the auth store, and the beforeEach guard above. After a successful login, read route.query.redirect and call router.push with it (validate that it starts with / first, so a crafted link cannot send users to another site).
Guidelines
- Use
<script setup lang="ts">; avoid the Options API anddefineComponentin new code unless the project already uses them. reffor everything by default; read and write.valuein script, never in the template. Do not destructure areactive()object or a store's state; usetoRefsorstoreToRefs.- Always give
v-fora stable:key, and do not putv-forandv-ifon the same element: in Vue 3v-ifruns first and cannot see the loop variable. Filter with acomputedor put thev-ifon a wrapper element around the loop. - Check
res.okbeforeres.json();fetchdoes not reject on HTTP errors. - Keep secrets out of client code: anything in
import.meta.envthat starts withVITE_is public. - A client-side route guard is a convenience, not security; the API must enforce authorization.
- Check
package.jsonfirst: older projects may be on Vue 2 (end of life), Vue CLI or Pinia 2, where some of the above differs.
Prerequisites
How it compares
Unlike manual DOM manipulation, this framework uses reactive refs and computed values to automatically sync the user interface with application state.
Compared to similar skills
vue side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| vue (this skill) | 0 | 5mo | Review | Intermediate |
| vue-best-practices | 19 | 7mo | No flags | Intermediate |
| vue-pages | 1 | 9mo | No flags | Intermediate |
| pinia | 1 | 9mo | No flags | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by TerminalSkills
View all by TerminalSkills →You might also like
vue-best-practices
antfu
MUST be used for Vue.js tasks. Strongly recommends Composition API with `<script setup>` and TypeScript as the standard approach. Covers Vue 3, SSR, Volar, vue-tsc. Load for any Vue, .vue files, Vue Router, Pinia, or Vite with Vue work. ALWAYS use Composition API unless the project explicitly requires Options API.
vue-pages
JaguarJack
Generate Vue frontend pages using catch-table for CatchAdmin module with full component features.
pinia
antfu
Pinia official Vue state management library, type-safe and extensible. Use when defining stores, working with state/getters/actions, or implementing store patterns in Vue apps.
vue
valibali
Use when editing .vue files, creating Vue 3 components, writing composables, or testing Vue code - provides Composition API patterns, props/emits best practices, VueUse integration, and reactive destructuring guidance
vue-testing-best-practices
vuejs-ai
Use for Vue.js testing. Covers Vitest, Vue Test Utils, component testing, mocking, testing patterns, and Playwright for E2E testing.
vue-debug-guides
vuejs-ai
Vue 3 debugging and error handling for runtime errors, warnings, async failures, and SSR/hydration issues. Use when diagnosing or fixing Vue issues.