Vue 3 and Nuxt · Vue 3 Fundamentals
Single-File Components: Props, Slots and Rendering
Build typed single-file components with defineProps and defaults, compose them with default and named slots, and render lists and conditions.
What you'll build
A ProductCard single-file component with typed props and defaults, a computed label, conditional rendering for stock, and slots for the card's content and actions, used from a page that renders a product list. It runs as a server-rendered Nuxt page, so the output is the HTML a visitor receives.
The idea
- A single-file component (
.vue) has<script setup>,<template>and optional<style>. defineProps<T>()declares typed props; destructured props keep reactivity in Vue 3.5+ and can have defaults (const { badge = "New" } = defineProps<...>()).- Slots let parents pass template content: the default slot and named slots (
<template #actions>). v-forneeds a stable:key;v-if/v-elserender conditionally;:classbinds classes from state.- Props flow down, events flow up; never mutate a prop.
The code
// file: nuxt.config.ts
export default defineNuxtConfig({ compatibilityDate: "2026-10-01", devtools: { enabled: false } });
// file: app/app.vue
<template>
<main><NuxtPage /></main>
</template>
// file: app/components/ProductCard.vue
<script setup lang="ts">
const { name, price, stock, badge = "" } = defineProps<{
name: string;
price: number;
stock: number;
badge?: string;
}>();
const priceLabel = computed(() => `$${price.toFixed(2)}`);
</script>
<template>
<article :class="{ 'sold-out': stock === 0 }">
<h2>{{ name }} <small v-if="badge">({{ badge }})</small></h2>
<p>{{ priceLabel }}</p>
<p v-if="stock === 0">Sold out</p>
<p v-else-if="stock < 5">Only {{ stock }} left</p>
<slot />
<footer><slot name="actions" /></footer>
</article>
</template>
// file: app/pages/index.vue
<script setup lang="ts">
const products = [
{ id: 1, name: "Mug", price: 8.5, stock: 12, badge: "Bestseller", description: "Stoneware, 350 ml" },
{ id: 2, name: "Teapot", price: 24, stock: 3, description: "Cast iron, 1 l" },
{ id: 3, name: "Kettle", price: 31.9, stock: 0, description: "Whistling, 2 l" },
];
</script>
<template>
<h1>Kitchen shop</h1>
<ProductCard v-for="p in products" :key="p.id" :name="p.name" :price="p.price" :stock="p.stock" :badge="p.badge">
<p>{{ p.description }}</p>
<template #actions>
<button :disabled="p.stock === 0">Add to cart</button>
</template>
</ProductCard>
</template>
// request: GET /Output
GET / -> 200 Kitchen shop Mug (Bestseller) $8.50 Stoneware, 350 ml Add to cart Teapot $24.00 Only 3 left Cast iron, 1 l Add to cart Kettle $31.90 Sold out Whistling, 2 l Add to cart
Run this on your computer — see “Get Started” for setup.
Nuxt auto-imports computed and components from app/components; in a plain Vue project you import them.
Common mistakes
- Mutating props (
props.stock--); emit an event or usedefineModelinstead. - Using the array index as
:keyfor lists that can reorder or change. v-ifandv-foron the same element; filter with a computed list instead.- Passing many props through several layers ("prop drilling") where a slot or provide/inject would be simpler.
Key points
- defineProps with TypeScript types and destructured defaults.
- Default and named slots for flexible composition.
- Stable keys for lists; props down, events up.
Further reading: Components basics · Props · Slots
Practice questions
Why must you not mutate props?
Answer
Props are owned by the parent; mutating them breaks one-way data flow and is overwritten on the next parent render. Emit an event or use defineModel so the parent updates the value.
What are named slots for?
Answer
They let a parent pass several pieces of template content into specific places in a child component, such as a header and an actions area.
Why does v-for need a key?
Answer
Keys let Vue track each item's identity so it can reuse and reorder elements correctly when the list changes; use stable IDs, not indexes.
Open in the app to mark lessons complete, take notes and track your progress.