Claude Code Plugins

Community-maintained marketplace

Feedback

Expert knowledge for working with VueJS, the preferred frontend framework we use along with Vite as a bundler

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name vuejs
description Expert knowledge for working with VueJS, the preferred frontend framework we use along with Vite as a bundler

VueJS

VueJS is the preferred frontend framework we use along with Vite as a bundler.

Core Principles

  • Applications should stay current with Vue 3
  • Always use Composition API (never Options API)
  • Default to <script setup> syntax over plain <script> blocks
  • Strong TypeScript integration is expected
  • Use VueUse for common composable patterns
  • Use Vite as the standard bundler
  • Styling / CSS
    • Use the Unocss framework over Tailwind CSS
    • Use both flex and grid layouts but prefer grid in cases where both are appropriate.

Composition API Overview

The Composition API organizes component logic by feature rather than by option type. It enables better code reuse through composables and provides superior TypeScript support.

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

// Reactive state
const count = ref(0)

// Computed property
const doubled = computed(() => count.value * 2)

// Methods
function increment() {
  count.value++
}

// Lifecycle
onMounted(() => {
  console.log('Component mounted')
})
</script>

<template>
  <button @click="increment">Count: {{ count }} (doubled: {{ doubled }})</button>
</template>

Script Setup

<script setup> is syntactic sugar that provides:

  • Less boilerplate code
  • Better runtime performance (template compiles in same scope)
  • Superior TypeScript type inference
  • Automatic exposure of top-level bindings to template

Compiler Macros

These macros are globally available in <script setup> without imports:

defineProps

// Runtime declaration
const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 }
})

// Type-based declaration (preferred)
const props = defineProps<{
  title: string
  count?: number
}>()

// With defaults (type-based)
const props = withDefaults(defineProps<{
  title: string
  count?: number
}>(), {
  count: 0
})

// Destructuring with defaults (3.5+)
const { title, count = 0 } = defineProps<{
  title: string
  count?: number
}>()

defineEmits

// Runtime declaration
const emit = defineEmits(['update', 'delete'])

// Type-based declaration (preferred)
const emit = defineEmits<{
  update: [id: number, value: string]
  delete: [id: number]
}>()

// Usage
emit('update', 1, 'new value')

defineModel

Creates two-way binding props for v-model:

// Default model (v-model)
const modelValue = defineModel<string>()
modelValue.value = 'hello' // emits 'update:modelValue'

// Named model (v-model:count)
const count = defineModel<number>('count', { default: 0 })

// With options
const title = defineModel<string>('title', {
  required: true,
  default: 'Untitled'
})

defineExpose

Explicitly expose properties to parent components via template refs:

const internalState = ref(0)
const publicMethod = () => { /* ... */ }

// Only publicMethod is accessible via ref
defineExpose({
  publicMethod
})

defineOptions

Declare component options (Vue 3.3+):

defineOptions({
  inheritAttrs: false,
  name: 'CustomComponent'
})

defineSlots

Type slot props (Vue 3.3+):

const slots = defineSlots<{
  default(props: { item: Item }): any
  header(): any
}>()

Accessing Slots and Attrs

import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()

Top-Level Await

Async operations are supported (requires <Suspense> wrapper):

<script setup>
const data = await fetch('/api/data').then(r => r.json())
</script>

Reactivity System

ref()

Wraps values in a reactive container. Preferred for most cases.

import { ref } from 'vue'

const count = ref(0)
const user = ref<User | null>(null)

// Access/modify via .value
count.value++
user.value = { name: 'Ken' }

// Auto-unwraps in templates
// <template>{{ count }}</template> - no .value needed

Advantages:

  • Works with primitives and objects
  • Can be passed to functions while retaining reactivity
  • Explicit .value makes reactivity visible

reactive()

Makes objects deeply reactive without wrapping:

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: { name: 'Ken' }
})

// Direct property access
state.count++
state.user.name = 'Kenneth'

Limitations (prefer ref() instead):

  • Only works with objects, not primitives
  • Cannot replace entire object: state = { ... } breaks reactivity
  • Destructuring loses reactivity: const { count } = state is not reactive

computed()

Creates cached derived values:

import { ref, computed } from 'vue'

const firstName = ref('Ken')
const lastName = ref('Snyder')

// Read-only computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`)

// Writable computed (rare)
const fullNameWritable = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (value: string) => {
    const [first, last] = value.split(' ')
    firstName.value = first
    lastName.value = last
  }
})

Best practices:

  • Keep getters pure (no side effects, async, or DOM mutations)
  • Computed values are cached until dependencies change
  • Use methods when caching isn't beneficial

watch()

Watches specific reactive sources:

import { ref, watch } from 'vue'

const count = ref(0)
const user = ref<User | null>(null)

// Watch single ref
watch(count, (newValue, oldValue) => {
  console.log(`Count changed: ${oldValue} -> ${newValue}`)
})

// Watch multiple sources
watch([count, user], ([newCount, newUser], [oldCount, oldUser]) => {
  // ...
})

// Watch getter function
watch(
  () => user.value?.name,
  (newName) => console.log(`Name: ${newName}`)
)

// Options
watch(count, callback, {
  immediate: true,  // Run immediately on creation
  deep: true,       // Deep watch nested objects (use sparingly)
  once: true,       // Run only once (3.4+)
  flush: 'post'     // Run after DOM updates
})

// Stop watching
const stop = watch(count, callback)
stop() // Stop the watcher

watchEffect()

Automatically tracks all reactive dependencies accessed during execution:

import { ref, watchEffect } from 'vue'

const count = ref(0)
const name = ref('Ken')

// Automatically tracks count and name
watchEffect(() => {
  console.log(`Count: ${count.value}, Name: ${name.value}`)
})

// With cleanup
watchEffect((onCleanup) => {
  const controller = new AbortController()
  fetch('/api/data', { signal: controller.signal })

  onCleanup(() => controller.abort())
})

When to use which:

  • watch(): When you need old/new values, or want explicit control over dependencies
  • watchEffect(): When callback uses the same values it needs to watch

nextTick()

Wait for DOM updates after state changes:

import { ref, nextTick } from 'vue'

const count = ref(0)

async function increment() {
  count.value++
  await nextTick()
  // DOM is now updated
}

Lifecycle Hooks

All hooks must be called synchronously during setup():

import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onActivated,
  onDeactivated
} from 'vue'

// Most commonly used
onMounted(() => {
  // Component is mounted, DOM is available
  console.log('Mounted')
})

onUnmounted(() => {
  // Cleanup: remove event listeners, cancel timers
  console.log('Unmounted')
})

onUpdated(() => {
  // DOM has been updated after reactive state change
  console.log('Updated')
})

// Less common
onBeforeMount(() => { /* Before initial render */ })
onBeforeUpdate(() => { /* Before DOM update */ })
onBeforeUnmount(() => { /* Before unmounting */ })

// Error handling
onErrorCaptured((err, instance, info) => {
  console.error('Error captured:', err)
  return false // Prevent propagation
})

// Keep-alive specific
onActivated(() => { /* Component activated from cache */ })
onDeactivated(() => { /* Component deactivated to cache */ })

Component Patterns

Props

// Type-based declaration (preferred)
interface Props {
  title: string
  count?: number
  items: Item[]
  onUpdate?: (value: string) => void
}

const props = defineProps<Props>()

// With defaults
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  items: () => []  // Factory function for objects/arrays
})

// Destructuring with defaults
const { title, count = 0 } = defineProps<Props>()

One-way data flow: Props flow down, events flow up. Never mutate props directly.

// BAD - mutating prop
props.count++

// GOOD - emit event to parent
const emit = defineEmits<{ update: [count: number] }>()
emit('update', props.count + 1)

Emits

// Type-based declaration
const emit = defineEmits<{
  change: [id: number]
  update: [id: number, value: string]
  'update:modelValue': [value: string]
}>()

// Usage
emit('change', 1)
emit('update', 1, 'new value')

Slots

Parent component:

<template>
  <MyComponent>
    <!-- Default slot -->
    <p>Default content</p>

    <!-- Named slot -->
    <template #header>
      <h1>Header</h1>
    </template>

    <!-- Scoped slot -->
    <template #item="{ item, index }">
      <li>{{ index }}: {{ item.name }}</li>
    </template>
  </MyComponent>
</template>

Child component:

<script setup lang="ts">
interface Item {
  name: string
}

defineSlots<{
  default(): any
  header(): any
  item(props: { item: Item; index: number }): any
}>()
</script>

<template>
  <div>
    <header>
      <slot name="header" />
    </header>

    <main>
      <slot />  <!-- default slot -->
    </main>

    <ul>
      <li v-for="(item, index) in items" :key="item.id">
        <slot name="item" :item="item" :index="index" />
      </li>
    </ul>
  </div>
</template>

Provide/Inject

Dependency injection for deeply nested components:

// Parent component
import { provide, ref } from 'vue'
import type { InjectionKey } from 'vue'

// Type-safe injection key
const ThemeKey: InjectionKey<Ref<'light' | 'dark'>> = Symbol('theme')

const theme = ref<'light' | 'dark'>('light')

provide(ThemeKey, theme)

// Also provide mutation function
provide('setTheme', (newTheme: 'light' | 'dark') => {
  theme.value = newTheme
})
// Child component (any depth)
import { inject } from 'vue'

const theme = inject(ThemeKey)  // Ref<'light' | 'dark'> | undefined
const theme = inject(ThemeKey, ref('light'))  // With default

const setTheme = inject<(theme: 'light' | 'dark') => void>('setTheme')

TypeScript Integration

Typing Refs

import { ref, type Ref } from 'vue'

// Inferred
const count = ref(0)  // Ref<number>

// Explicit
const user = ref<User | null>(null)

// Complex types
interface State {
  items: Item[]
  loading: boolean
}
const state = ref<State>({ items: [], loading: false })

Typing Reactive

import { reactive } from 'vue'

interface State {
  count: number
  user: User | null
}

// Use interface annotation
const state: State = reactive({
  count: 0,
  user: null
})

Typing Computed

import { computed, type ComputedRef } from 'vue'

// Usually inferred
const doubled = computed(() => count.value * 2)  // ComputedRef<number>

// Explicit when needed
const user: ComputedRef<User | undefined> = computed(() =>
  users.value.find(u => u.id === selectedId.value)
)

Typing Event Handlers

function handleInput(event: Event) {
  const target = event.target as HTMLInputElement
  console.log(target.value)
}

function handleClick(event: MouseEvent) {
  console.log(event.clientX, event.clientY)
}

Typing Template Refs

import { ref, onMounted } from 'vue'

const inputRef = ref<HTMLInputElement | null>(null)
const componentRef = ref<InstanceType<typeof MyComponent> | null>(null)

onMounted(() => {
  inputRef.value?.focus()
  componentRef.value?.someMethod()
})
<template>
  <input ref="inputRef" />
  <MyComponent ref="componentRef" />
</template>

Composables

Composables are functions that encapsulate and reuse stateful logic using Composition API.

Naming Convention

Always prefix with use: useMouse, useFetch, useAuth

Creating a Composable

// 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 }
}
// Usage in component
import { useMouse } from '@/composables/useMouse'

const { x, y } = useMouse()

Composable with Arguments

// composables/useFetch.ts
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue'

export function useFetch<T>(url: MaybeRefOrGetter<string>) {
  const data = ref<T | null>(null)
  const error = ref<Error | null>(null)
  const loading = ref(false)

  async function fetchData() {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(toValue(url))
      data.value = await response.json()
    } catch (e) {
      error.value = e instanceof Error ? e : new Error('Unknown error')
    } finally {
      loading.value = false
    }
  }

  watchEffect(() => {
    fetchData()
  })

  return { data, error, loading, refetch: fetchData }
}

Best Practices

  1. Return refs, not reactive objects - Allows destructuring while maintaining reactivity
  2. Accept MaybeRefOrGetter - Use toValue() to handle refs, getters, or plain values
  3. Clean up side effects - Use onUnmounted or cleanup callbacks
  4. Call synchronously in setup - Don't call composables inside callbacks or conditions

Best Practices

Prefer ref() over reactive()

// Preferred
const count = ref(0)
const user = ref<User | null>(null)

// Avoid unless you have a specific reason
const state = reactive({ count: 0, user: null })

Keep Computed Getters Pure

// BAD
const doubled = computed(() => {
  fetch('/api/log')  // Side effect!
  return count.value * 2
})

// GOOD
const doubled = computed(() => count.value * 2)

Use Descriptive Variable Names

// BAD
const d = ref<User[]>([])

// GOOD
const users = ref<User[]>([])
const isLoading = ref(false)
const hasError = ref(false)

Avoid Mutating Props

// BAD
props.items.push(newItem)

// GOOD
emit('add-item', newItem)

Type Everything

// BAD
const props = defineProps(['title', 'count'])

// GOOD
const props = defineProps<{
  title: string
  count?: number
}>()

Co-locate Related Logic

// Group related state, computed, and methods together
// Feature 1: User management
const user = ref<User | null>(null)
const isLoggedIn = computed(() => user.value !== null)
async function login(credentials: Credentials) { /* ... */ }
async function logout() { /* ... */ }

// Feature 2: Theme
const theme = ref<'light' | 'dark'>('light')
const isDarkMode = computed(() => theme.value === 'dark')
function toggleTheme() { /* ... */ }

Common Patterns

Async Component Loading

import { defineAsyncComponent } from 'vue'

const AsyncModal = defineAsyncComponent(() =>
  import('./components/Modal.vue')
)

const AsyncModalWithOptions = defineAsyncComponent({
  loader: () => import('./components/Modal.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 3000
})

v-model with Components

<!-- Parent -->
<CustomInput v-model="searchQuery" />
<CustomInput v-model:title="title" v-model:content="content" />
<!-- CustomInput.vue -->
<script setup lang="ts">
const model = defineModel<string>({ required: true })
</script>

<template>
  <input :value="model" @input="model = ($event.target as HTMLInputElement).value" />
</template>

Conditional Rendering

<template>
  <!-- v-if for conditional rendering (removes from DOM) -->
  <div v-if="isLoading">Loading...</div>
  <div v-else-if="error">Error: {{ error.message }}</div>
  <div v-else>{{ data }}</div>

  <!-- v-show for frequent toggling (CSS display: none) -->
  <div v-show="isVisible">Toggleable content</div>
</template>

List Rendering

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>

  <!-- With index -->
  <li v-for="(item, index) in items" :key="item.id">
    {{ index }}: {{ item.name }}
  </li>

  <!-- Object iteration -->
  <div v-for="(value, key, index) in object" :key="key">
    {{ key }}: {{ value }}
  </div>
</template>

Event Handling

<template>
  <!-- Method handler -->
  <button @click="handleClick">Click</button>

  <!-- Inline handler -->
  <button @click="count++">Increment</button>

  <!-- With event -->
  <button @click="handleClick($event)">Click</button>

  <!-- Modifiers -->
  <button @click.stop="handleClick">Stop propagation</button>
  <button @click.prevent="handleSubmit">Prevent default</button>
  <input @keyup.enter="submit" />
  <input @keyup.ctrl.enter="submitWithCtrl" />
</template>

Form Handling

<script setup lang="ts">
import { ref, reactive } from 'vue'

const form = reactive({
  email: '',
  password: '',
  remember: false
})

const errors = ref<Record<string, string>>({})

async function handleSubmit() {
  errors.value = {}

  if (!form.email) {
    errors.value.email = 'Email is required'
  }

  if (Object.keys(errors.value).length === 0) {
    await submitForm(form)
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="form.email" type="email" />
    <span v-if="errors.email">{{ errors.email }}</span>

    <input v-model="form.password" type="password" />

    <label>
      <input v-model="form.remember" type="checkbox" />
      Remember me
    </label>

    <button type="submit">Submit</button>
  </form>
</template>

Related Resources

Core Tools

TypeScript & Language Tooling

Styling

  • UnoCSS - Instant on-demand Atomic CSS engine (Tailwind-compatible)

UI Components

  • UI Component Libraries - Comprehensive guide to Vue 3 UI ecosystem:
    • Full suites (Vuetify, PrimeVue, Quasar, Naive UI)
    • Headless libraries (Headless UI, Inspira UI)
    • Specialized: tables, charts, forms, date pickers, editors

Data Visualization

  • Chart.js - Flexible charting library with Vue 3 integration via vue-chartjs

Build Plugins

  • UnPlugins - Essential Vue build plugins:
    • unplugin-vue-router - File-based routing with TypeScript
    • unplugin-vue-components - Auto-import components
    • unplugin-vue-macros - Extended compiler macros
    • unplugin-auto-import - Auto-import APIs

Static Site Generation

  • Vite SSG - Static site generation for Vue 3 on Vite