Use when Vue component patterns including props, emits, slots, and provide/inject. Use when building reusable Vue components.
Generates Vue 3 components with TypeScript props, emits, slots, and provide/inject patterns.
npx claudepluginhub thebushidocollective/hanThis skill is limited to using the following tools:
Master Vue component patterns to build reusable, maintainable components with proper prop validation, events, and composition.
<script setup lang="ts">
interface Props {
title: string;
count?: number;
items: string[];
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
});
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
</template>
<script setup lang="ts">
import type { PropType } from 'vue';
type Status = 'pending' | 'success' | 'error';
interface User {
id: number;
name: string;
email: string;
}
interface Props {
// Literal types
status: Status;
// Complex objects
user: User;
// Functions
onUpdate: (value: string) => void;
// Generic arrays
tags: string[];
// Object arrays
users: User[];
// Nullable
description: string | null;
// Union types
value: string | number;
}
const props = defineProps<Props>();
</script>
<script setup lang="ts">
import type { PropType } from 'vue';
type ButtonSize = 'sm' | 'md' | 'lg';
const props = defineProps({
// Type checking
title: {
type: String,
required: true
},
// Default values
count: {
type: Number,
default: 0
},
// Multiple types
value: {
type: [String, Number],
required: true
},
// Object with type
user: {
type: Object as PropType<{ name: string; age: number }>,
required: true
},
// Array with type
tags: {
type: Array as PropType<string[]>,
default: () => []
},
// Custom validator
size: {
type: String as PropType<ButtonSize>,
default: 'md',
validator: (value: string) => ['sm', 'md', 'lg'].includes(value)
},
// Complex validator
email: {
type: String,
validator: (value: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
},
// Function prop
onClick: {
type: Function as PropType<(id: number) => void>,
required: false
}
});
</script>
<script setup lang="ts">
interface Props {
title?: string;
count?: number;
items?: string[];
user?: {
name: string;
email: string;
};
options?: {
enabled: boolean;
timeout: number;
};
}
// Simple defaults
const props = withDefaults(defineProps<Props>(), {
title: 'Default Title',
count: 0
});
// Function defaults for objects/arrays
const propsWithComplex = withDefaults(defineProps<Props>(), {
title: 'Default',
count: 0,
items: () => [],
user: () => ({ name: 'Guest', email: 'guest@example.com' }),
options: () => ({ enabled: true, timeout: 5000 })
});
</script>
<script setup lang="ts">
// Define emit types
const emit = defineEmits<{
// No payload
close: [];
// Single payload
update: [value: string];
// Multiple payloads
change: [id: number, value: string];
// Object payload
submit: [data: { name: string; email: string }];
}>();
function handleClose() {
emit('close');
}
function handleUpdate(value: string) {
emit('update', value);
}
function handleChange(id: number, value: string) {
emit('change', id, value);
}
function handleSubmit() {
emit('submit', { name: 'John', email: 'john@example.com' });
}
</script>
<script setup lang="ts">
const emit = defineEmits({
// Basic event
click: null,
// Validation
update: (value: number) => {
return value >= 0;
},
// Complex validation
submit: (payload: { email: string; password: string }) => {
if (!payload.email || !payload.password) {
console.warn('Invalid submit payload');
return false;
}
return true;
}
});
</script>
<!-- CustomInput.vue -->
<script setup lang="ts">
interface Props {
modelValue: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
'update:modelValue': [value: string];
}>();
function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
emit('update:modelValue', target.value);
}
</script>
<template>
<input
:value="modelValue"
@input="handleInput"
type="text"
/>
</template>
<!-- Usage -->
<script setup lang="ts">
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const text = ref('');
</script>
<template>
<CustomInput v-model="text" />
</template>
<!-- RangeSlider.vue -->
<script setup lang="ts">
interface Props {
min: number;
max: number;
}
const props = defineProps<Props>();
const emit = defineEmits<{
'update:min': [value: number];
'update:max': [value: number];
}>();
</script>
<template>
<div>
<input
type="range"
:value="min"
@input="emit('update:min', Number($event.target.value))"
/>
<input
type="range"
:value="max"
@input="emit('update:max', Number($event.target.value))"
/>
</div>
</template>
<!-- Usage -->
<script setup lang="ts">
import { ref } from 'vue';
const minValue = ref(0);
const maxValue = ref(100);
</script>
<template>
<RangeSlider v-model:min="minValue" v-model:max="maxValue" />
</template>
<!-- Card.vue -->
<template>
<div class="card">
<header v-if="$slots.header">
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer v-if="$slots.footer">
<slot name="footer" />
</footer>
</div>
</template>
<!-- Usage -->
<template>
<Card>
<template #header>
<h1>Card Title</h1>
</template>
<p>Card content goes here</p>
<template #footer>
<button>Action</button>
</template>
</Card>
</template>
<!-- List.vue -->
<script setup lang="ts" generic="T">
interface Props {
items: T[];
}
const props = defineProps<Props>();
</script>
<template>
<div>
<div v-for="(item, index) in items" :key="index">
<slot :item="item" :index="index" />
</div>
</div>
</template>
<!-- Usage -->
<script setup lang="ts">
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];
</script>
<template>
<List :items="users">
<template #default="{ item, index }">
<div>
{{ index + 1 }}. {{ item.name }} - {{ item.email }}
</div>
</template>
</List>
</template>
<!-- Button.vue -->
<template>
<button>
<slot>
Click Me
</slot>
</button>
</template>
<!-- Custom content -->
<Button>Custom Text</Button>
<!-- Uses fallback -->
<Button />
<!-- DynamicSlots.vue -->
<script setup lang="ts">
import { useSlots } from 'vue';
const slots = useSlots();
// Check if slot exists
const hasHeader = !!slots.header;
// Access slot props
const headerProps = slots.header?.();
</script>
<template>
<div>
<div v-if="hasHeader" class="header">
<slot name="header" />
</div>
<slot />
</div>
</template>
<!-- Mouse.vue - Renderless component -->
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
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);
});
</script>
<template>
<slot :x="x" :y="y" />
</template>
<!-- Usage -->
<template>
<Mouse v-slot="{ x, y }">
<p>Mouse position: {{ x }}, {{ y }}</p>
</Mouse>
</template>
<!-- Parent.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue';
const theme = ref('dark');
function toggleTheme() {
theme.value = theme.value === 'dark' ? 'light' : 'dark';
}
provide('theme', theme);
provide('toggleTheme', toggleTheme);
</script>
<!-- Child.vue (any depth) -->
<script setup lang="ts">
import { inject, type Ref } from 'vue';
const theme = inject<Ref<string>>('theme');
const toggleTheme = inject<() => void>('toggleTheme');
</script>
<template>
<div :class="theme">
<button @click="toggleTheme">Toggle Theme</button>
</div>
</template>
// types.ts
import type { InjectionKey, Ref } from 'vue';
export interface AppConfig {
apiUrl: string;
timeout: number;
}
export interface User {
id: number;
name: string;
email: string;
}
export const ConfigKey: InjectionKey<AppConfig> = Symbol('config');
export const UserKey: InjectionKey<Ref<User | null>> = Symbol('user');
// Provider
<script setup lang="ts">
import { provide, ref } from 'vue';
import { ConfigKey, UserKey } from './types';
const config: AppConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
const user = ref<User | null>(null);
provide(ConfigKey, config);
provide(UserKey, user);
</script>
// Consumer
<script setup lang="ts">
import { inject } from 'vue';
import { ConfigKey, UserKey } from './types';
const config = inject(ConfigKey);
const user = inject(UserKey);
// Fully typed!
console.log(config?.apiUrl);
console.log(user?.value?.name);
</script>
<!-- App.vue -->
<script setup lang="ts">
import { provide, reactive, readonly } from 'vue';
interface State {
count: number;
user: { name: string };
}
const state = reactive<State>({
count: 0,
user: { name: 'John' }
});
function increment() {
state.count++;
}
// Provide readonly to prevent mutations
provide('state', readonly(state));
provide('increment', increment);
</script>
<!-- Consumer -->
<script setup lang="ts">
import { inject } from 'vue';
const state = inject('state');
const increment = inject('increment');
</script>
<template>
<div>
<p>Count: {{ state.count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import BaseButton from './components/BaseButton.vue';
import BaseInput from './components/BaseInput.vue';
const app = createApp(App);
// Register globally
app.component('BaseButton', BaseButton);
app.component('BaseInput', BaseInput);
app.mount('#app');
// Use anywhere without importing
<template>
<BaseButton>Click</BaseButton>
<BaseInput v-model="text" />
</template>
<script setup lang="ts">
import BaseButton from './components/BaseButton.vue';
import BaseInput from './components/BaseInput.vue';
// Automatically registered in this component
</script>
<template>
<BaseButton>Click</BaseButton>
<BaseInput v-model="text" />
</template>
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
export default defineConfig({
plugins: [
vue(),
Components({
// Auto import from components directory
dirs: ['src/components'],
// Generate types
dts: true
})
]
});
// Now use components without importing
<template>
<BaseButton>No import needed!</BaseButton>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
// Basic async component
const AsyncComponent = defineAsyncComponent(() =>
import('./components/Heavy.vue')
);
// With loading and error states
const AsyncWithOptions = defineAsyncComponent({
loader: () => import('./components/Heavy.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000
});
</script>
<template>
<Suspense>
<AsyncComponent />
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<!-- Modal.vue -->
<script setup lang="ts">
import { ref } from 'vue';
interface Props {
show: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{
close: [];
}>();
</script>
<template>
<Teleport to="body">
<div v-if="show" class="modal-backdrop" @click="emit('close')">
<div class="modal" @click.stop>
<slot />
</div>
</div>
</Teleport>
</template>
<style scoped>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background: white;
padding: 2rem;
border-radius: 8px;
}
</style>
<!-- Usage -->
<script setup lang="ts">
import { ref } from 'vue';
import Modal from './Modal.vue';
const showModal = ref(false);
</script>
<template>
<button @click="showModal = true">Open Modal</button>
<Modal :show="showModal" @close="showModal = false">
<h2>Modal Content</h2>
<p>This is teleported to body!</p>
</Modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import TabA from './TabA.vue';
import TabB from './TabB.vue';
import TabC from './TabC.vue';
const currentTab = ref('TabA');
const tabs = {
TabA,
TabB,
TabC
};
</script>
<template>
<div>
<button
v-for="(_, tab) in tabs"
:key="tab"
@click="currentTab = tab"
>
{{ tab }}
</button>
<!-- Cache inactive components -->
<KeepAlive>
<component :is="tabs[currentTab]" />
</KeepAlive>
<!-- Include/exclude specific components -->
<KeepAlive :include="['TabA', 'TabB']">
<component :is="tabs[currentTab]" />
</KeepAlive>
<!-- Max cached instances -->
<KeepAlive :max="3">
<component :is="tabs[currentTab]" />
</KeepAlive>
</div>
</template>
// withLoading.ts
import { defineComponent, h, ref, onMounted } from 'vue';
export function withLoading(Component: any, loadFn: () => Promise<void>) {
return defineComponent({
setup(props, { attrs, slots }) {
const loading = ref(true);
const error = ref<Error | null>(null);
onMounted(async () => {
try {
await loadFn();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
});
return () => {
if (loading.value) {
return h('div', 'Loading...');
}
if (error.value) {
return h('div', `Error: ${error.value.message}`);
}
return h(Component, { ...props, ...attrs }, slots);
};
}
});
}
// Usage
const UserProfile = withLoading(
UserProfileComponent,
async () => {
// Load user data
}
);
Use vue-component-patterns when building modern, production-ready applications that require:
withDefaults for optional propsv-model for two-way binding - Especially for form inputsonUnmounted<script setup lang="ts">
interface Props {
modelValue: string;
label?: string;
error?: string;
placeholder?: string;
required?: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{
'update:modelValue': [value: string];
blur: [];
}>();
</script>
<template>
<div class="form-field">
<label v-if="label">
{{ label }}
<span v-if="required" class="required">*</span>
</label>
<input
:value="modelValue"
:placeholder="placeholder"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
@blur="emit('blur')"
/>
<span v-if="error" class="error">{{ error }}</span>
</div>
</template>
<script setup lang="ts" generic="T">
interface Column<T> {
key: keyof T;
label: string;
sortable?: boolean;
}
interface Props {
data: T[];
columns: Column<T>[];
}
const props = defineProps<Props>();
const emit = defineEmits<{
sort: [column: keyof T];
rowClick: [item: T];
}>();
</script>
<template>
<table>
<thead>
<tr>
<th
v-for="col in columns"
:key="String(col.key)"
@click="col.sortable && emit('sort', col.key)"
>
{{ col.label }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in data"
:key="index"
@click="emit('rowClick', item)"
>
<td v-for="col in columns" :key="String(col.key)">
<slot :name="`cell-${String(col.key)}`" :item="item">
{{ item[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.