Components are the heart of Vue: self-contained, reusable pieces of UI that combine into full applications. A page becomes a tree -- App renders a Header, a ProductList, which renders many ProductCards. Data flows down this tree through props. This lesson shows how to define components, register them, and pass data safely.
With Single File Components, each component is a .vue file. Here is ProductCard.vue:
<script setup>
defineProps({
name: { type: String, required: true },
price: { type: Number, required: true },
inStock: { type: Boolean, default: true }
})
</script>
<template>
<article class="card">
<h3>{{ name }}</h3>
<p>${{ price.toFixed(2) }}</p>
<p v-if="!inStock">Currently unavailable</p>
</article>
</template>And the parent uses it by importing it -- with <script setup>, imported components are automatically available in the template:
<script setup>
import ProductCard from './ProductCard.vue'
import { ref } from 'vue'
const products = ref([
{ id: 1, name: 'Keyboard', price: 45, inStock: true },
{ id: 2, name: 'Monitor', price: 199, inStock: false }
])
</script>
<template>
<ProductCard
v-for="p in products"
:key="p.id"
:name="p.name"
:price="p.price"
:in-stock="p.inStock"
/>
</template>Note the casing convention: props are declared in camelCase (inStock) and passed in kebab-case in templates (:in-stock). Component tags in SFCs are typically written in PascalCase.
defineProps is a compile-time macro (no import needed). The object syntax buys you runtime validation:
defineProps({
title: String, // type check only
likes: { type: Number, default: 0 },
tags: { type: Array, default: () => [] }, // arrays/objects need factory defaults
status: {
type: String,
validator: (v) => ['draft', 'published'].includes(v)
}
})If a parent passes the wrong type, Vue logs a console warning in development -- catching bugs early. Static values need no colon (title="Hello"); anything dynamic or non-string does (:likes="42").
Props flow down only. A child must never mutate a prop:
const props = defineProps({ initialCount: Number })
// BAD: props.initialCount++
// GOOD: use the prop as a seed for local state…
const count = ref(props.initialCount)
// …or derive from it
const doubled = computed(() => props.initialCount * 2)This rule keeps data ownership clear: whoever declares the state mutates it. When a child needs to change parent data, it emits an event instead -- the subject of the next lesson.
Attributes you pass that are not declared as props -- class, id, data-* -- automatically land on the component's root element. <ProductCard class="featured" /> adds the class to the <article>. Multiple root nodes or custom targets require inheritAttrs: false and v-bind="$attrs", a technique worth remembering once your components grow wrappers.
A practical decomposition for a storefront page:
App
├── SiteHeader
├── ProductFilters
└── ProductList
└── ProductCard (× n)
└── PriceTagGuidelines that scale well:
ProductCard.vue) and prefer multi-word names to avoid clashing with HTML elements..vue files; import them and use them as tags in <script setup>.defineProps, including types, defaults, and validators.Next up: Component Events and v-model on Components -- how children talk back to parents.