Vue 3 tutorial – components and props with defineProps
verified on 2 September 2026 · 5 min
A component is a .vue file with its own template, its own state and its own logic. Import it into <script setup> and it is usable in the template right away. The parent hands it data through props, declared with defineProps and bound with a colon.
Everything has lived in a single file for seven chapters. That does not scale. A component is a self-contained piece of interface, with its own template, its own state and its own logic, that you reuse and nest.
Extracting the product card
Create src/components/ProductDisplay.vue and move everything to do with the product into it: the image, the title, the availability, the details, the swatches, the button.
<script setup>
import { computed, ref } from 'vue'
const product = ref('T-Shirt')
const brand = ref('Gekkode')
const selectedVariant = ref(0)
const details = ref(['60 % coton', '30 % laine', '10 % polyester'])
const variants = ref([
{ id: 2234, color: '#2563eb', image: '/images/t-shirt-bleu.svg', quantity: 20 },
{ id: 2235, color: '#dc2626', image: '/images/t-shirt-rouge.svg', quantity: 0 }
])
const title = computed(() => `${brand.value} ${product.value}`)
const image = computed(() => variants.value[selectedVariant.value].image)
const inStock = computed(() => variants.value[selectedVariant.value].quantity > 0)
function updateVariant(index) {
selectedVariant.value = index
}
</script>
<template>
<div class="product-display">
<div class="product-image">
<img :src="image" :alt="title">
</div>
<div class="product-info">
<h1>{{ title }}</h1>
<p v-if="inStock">En stock</p>
<p v-else>En rupture</p>
<ul>
<li v-for="detail in details" :key="detail">{{ detail }}</li>
</ul>
<div class="variants-wrapper">
<div
v-for="(variant, index) in variants"
:key="variant.id"
class="color-circle"
:style="{ backgroundColor: variant.color }"
@mouseover="updateVariant(index)"
></div>
</div>
<button class="button" :class="{ disabledButton: !inStock }" :disabled="!inStock">
Ajouter au panier
</button>
</div>
</div>
</template>The cart does not go with it: it does not belong to the product. App.vue comes down to this:
<script setup>
import { ref } from 'vue'
import ProductDisplay from './components/ProductDisplay.vue'
const cart = ref(0)
</script>
<template>
<div class="cart">Panier ({{ cart }})</div>
<ProductDisplay />
</template>A component imported into <script setup> can be used in the template straight away: no extra declaration.
That is expected, and it is what the next chapter is about. cart now lives in the parent, out of reach of the child component. We have also taken the @click off the button: leaving it there would have thrown an error, since addToCart no longer exists in this component.
Naming a component
Two conventions live side by side. In PascalCase, <ProductDisplay />, you can tell a component from an HTML tag at a glance, and your editor can jump to its definition. In kebab-case, <product-display></product-display>, you stay compatible with a template written directly in an HTML page.
Inside a .vue file, go for PascalCase.
A component has its own scope
Add three cards at once:
<template>
<ProductDisplay />
<ProductDisplay />
<ProductDisplay />
</template>Three cards appear, independent of one another: hovering a swatch on the first does not touch the others. Each instance runs its own <script setup> and holds its own selectedVariant.
Each card also keeps its own selected variant.
This is also why the state of a component is declared inside a function. If variants were a plain object shared at module level, the three cards would trample on each other.
Remove the two extra cards before carrying on.
Props
An isolated scope raises a question: how does the parent hand information to the child? Through a prop, a custom attribute that the component declares it accepts.
Let us add shipping, free for premium customers. The information belongs to the parent:
<script setup>
const premium = ref(true)
</script>
<template>
<div class="cart">Panier ({{ cart }})</div>
<ProductDisplay :premium="premium" />
</template>And the child declares it:
<script setup>
const props = defineProps({
premium: { type: Boolean, required: true }
})
const shipping = computed(() => (props.premium ? 'Gratuite' : '2,99 €'))
</script>
<template>
<p>Livraison : {{ shipping }}</p>
</template>defineProps is available without an import inside <script setup>: it is a compiler macro, not an ordinary function.
In the template, you write {{ premium }} directly. In the script, you have to go through the returned object: props.premium.
Declaring props properly
The long form lets you validate:
const props = defineProps({
premium: { type: Boolean, required: true },
titre: { type: String, default: 'Sans titre' },
tailles: { type: Array, default: () => [] },
note: {
type: Number,
validator: (valeur) => valeur >= 0 && valeur <= 5
}
})The default value of an array or an object has to be returned by a function, otherwise every instance would share the same object.
:premium="premium" passes the value of the variable. premium="premium", without the colon, passes the string "premium". Since a non-empty string is truthy, the bug goes unnoticed until the day the value ought to be false. Vue does warn in the console about a Boolean expected and a String received.
Props go down, they do not come back up
A prop travels in one direction only: from parent to child. The child must not modify it, Vue prints a warning if you try.
If the child needs a local version it can change, copy it into a ref. If it needs to trigger a change in the parent, it has to send a signal upwards: that is what the next chapter covers.
Props are declared in the props option, with the same validation syntax, and read through this.premium. Child components also have to be registered in the components option.
ProductDetails.vue component that receives the array as a prop. Then display two product cards and check that each one really does have its own selected variant.Common errors
premium="premium" passes the string “premium”, which is always truthy. Write :premium="premium". Vue does report a Boolean expected, String received.