Single File Components
Single File Components bundle template, script, and styles in one .vue file. The <script setup> syntax makes component code feel like writing a small JS module — declarative, reactive, scoped CSS by default, and excellent TypeScript ergonomics.
script setup, props, emits, slots
EXAMPLE
<!-- 1) Skeleton — the three blocks of a Vue SFC -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
onMounted(() => {
console.log('mounted');
});
function reset() { count.value = 0; }
</script>
<template>
<div class="counter">
<p>Count: {{ count }} (doubled: {{ doubled }})</p>
<button @click="count++">+1</button>
<button @click="reset">reset</button>
</div>
</template>
<style scoped>
.counter { padding: 1rem; border: 1px solid #ddd; border-radius: 8px; }
button { margin-right: 0.5rem; }
</style>
<!-- 2) Props — typed and validated -->
<script setup lang="ts">
interface Props {
title: string;
count?: number;
items: string[];
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
});
props.items; // string[]
</script>
<!-- 3) Emits — type-safe events -->
<script setup lang="ts">
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
(e: 'submit'): void;
}>();
function handleInput(event: Event) {
const target = event.target as HTMLInputElement;
emit('update:modelValue', target.value);
}
</script>
<!-- 4) v-model on custom components — implicit emit('update:modelValue') + prop -->
<script setup lang="ts">
defineProps<{ modelValue: string }>();
defineEmits<{ (e: 'update:modelValue', v: string): void }>();
</script>
<template>
<input :value="modelValue" @input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)" />
</template>
<!-- Use like: <MyInput v-model="name" /> -->
<!-- 5) defineModel — Vue 3.4+ shorthand for two-way binding -->
<script setup lang="ts">
const modelValue = defineModel<string>(); // ref<string | undefined>
const count = defineModel<number>('count', { default: 0 });
function reset() {
modelValue.value = '';
count.value = 0;
}
</script>
<template>
<input v-model="modelValue" />
<p>{{ count }}</p>
</template>
<!-- 6) Slots — default + named + scoped -->
<!-- Card.vue -->
<template>
<article class="card">
<header><slot name="title" /></header>
<div><slot /></div>
<footer><slot name="actions" :close="close" /></footer>
</article>
</template>
<script setup lang="ts">
function close() { /* close logic */ }
</script>
<!-- Caller -->
<Card>
<template #title><h3>Profile</h3></template>
<p>email: mara@example.com</p>
<template #actions="{ close }">
<button @click="close">Close</button>
</template>
</Card>
<!-- 7) defineExpose — control what parent refs see -->
<script setup lang="ts">
import { ref } from 'vue';
const input = ref<HTMLInputElement | null>(null);
function focus() { input.value?.focus(); }
defineExpose({ focus }); // parent can call (childRef.value as any).focus()
</script>
<!-- 8) Scoped styles + CSS modules + :deep -->
<style scoped>
.title { color: var(--brand); } /* scoped to this component */
.title :deep(.child) { color: red; } /* reach into a child component's DOM */
</style>
<style module>
/* CSS Modules — generates locally scoped class names accessible as $style.foo */
.title { font-weight: 600; }
</style>
<template>
<h2 :class="$style.title">…</h2>
</template>
<!-- 9) Component composition — composables -->
<!-- /composables/useCounter.ts -->
<script lang="ts">
import { ref } from 'vue';
export function useCounter(initial = 0) {
const n = ref(initial);
function inc() { n.value++; }
function reset() { n.value = initial; }
return { n, inc, reset };
}
</script>
<!-- In a SFC -->
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter';
const { n, inc } = useCounter(10);
</script>
<!-- 10) Provide / inject — pass data without props -->
<!-- Parent -->
<script setup lang="ts">
import { provide, ref } from 'vue';
import type { InjectionKey } from 'vue';
export const ThemeKey: InjectionKey<ReturnType<typeof useTheme>> = Symbol();
const theme = useTheme();
provide(ThemeKey, theme);
</script>
<!-- Descendant -->
<script setup lang="ts">
import { inject } from 'vue';
import { ThemeKey } from '@/components/Provider.vue';
const theme = inject(ThemeKey);
if (!theme) throw new Error('ThemeProvider missing');
</script>
<!-- 11) Async components — code-split a heavy widget -->
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
const Chart = defineAsyncComponent(() => import('./HeavyChart.vue'));
</script>
<template>
<Suspense>
<template #default><Chart :data="data" /></template>
<template #fallback>Loading…</template>
</Suspense>
</template>
<!-- 12) Templates: directives at a glance -->
<!-- v-if / v-else-if / v-else conditional rendering (removes from DOM) -->
<!-- v-show toggles 'display: none' -->
<!-- v-for="item in items" :key="item.id" list rendering — always provide :key -->
<!-- v-bind / :prop bind data
<!-- v-on / @event listen for events -->
<!-- v-model two-way binding -->
<!-- v-html sets innerHTML; sanitise first -->
<!-- v-text sets textContent -->
<!-- v-pre skip compilation for the element -->
<!-- v-once render once, cache -->
<!-- 13) Lifecycle hooks (Composition API) -->
<script setup lang="ts">
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, onErrorCaptured } from 'vue';
onMounted(() => { /* DOM available */ });
onUnmounted(() => { /* cleanup timers, subscriptions */ });
onErrorCaptured((err, instance, info) => { console.error(err); return false; });
</script>
<!-- 14) Tooling -->
<!-- <script setup> requires the @vitejs/plugin-vue build pipeline -->
<!-- TypeScript: 'lang="ts"'; install Volar (the official VS Code extension) -->
<!-- ESLint: eslint-plugin-vue + @vue/eslint-config-typescript -->
<!-- Prettier: works out of the box on .vue files -->
<!-- 15) Common bugs -->
<!-- • Forgot 'setup' on the script tag — Composition API utilities don't work -->
<!-- • Using a ref's value inside the template — refs auto-unwrap, don't write '.value' there -->
<!-- • Mutating a prop — log warning + change doesn't reflect; emit an update instead -->
<!-- • Async setup() without <Suspense> — fails silently -->
<!-- • Scoped styles can't reach into <Teleport> destinations — use :deep() or global styles -->
<!-- • Multiple root elements need <Suspense> wrapper if used inside <Transition> -->
<!-- • defineProps + defineEmits + defineModel are MACROS — Vue compiles them away; don't import them -->
Why it matters
<script setup> is the modern Vue idiom — concise, type-safe, and built around composables you can extract and reuse. Use defineProps with TypeScript interfaces, defineEmits for typed events, and defineModel (Vue 3.4+) to skip the boilerplate of two-way binding. Scoped styles keep CSS local; composables keep logic local.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
<!-- App.vue -->
<script setup>
import { ref } from 'vue';
const msg = ref('hello');
</script>
<template>
<h1>{{ msg }}</h1>
</template>
<style scoped>
h1 { color: #42b883; }
</style>
Try it Yourself »
Discussion
Loading…