Passing Data In
Declare a component's inputs with defineProps, read them in the template and in script, and understand why a prop is read-only.
const props = defineProps<{ title: string; unreadCount?: number }>()A prop is a named input that a parent passes into a component. Props are what make a component reusable: one UserCard renders any user.
Declaring them
Declare props with defineProps inside <script setup>. It is a compile-time macro, so it needs no import, and in TypeScript you pass the shape as a type argument. Declared props are available in the template by name. In script you read them through the object defineProps returns, as props.name.
Casing and shorthand
Declare in camelCase and pass in kebab-case: unreadCount is written :unread-count="3". :unread-count is shorthand for v-bind:unread-count and they are the same directive. Since Vue 3.4 there is a same-name shorthand too: :title with no value means :title="title".
One way only
Props are read-only. Data flows down from the parent, and the child cannot assign to a prop; the development build logs a warning if you try. When you need a value the child can change, copy the prop into a ref and treat it as a starting value. When you need a changed version, derive it with computed.
What trips people up
Read-only applies to the binding, not to the contents. props.user.name = 'Ada' raises no warning and quietly edits the parent's object, because objects are passed by reference.
Example
<!-- src/components/UserCard.vue -->
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
name: string
email: string
unreadCount?: number
}>()
const initials = computed(() =>
props.name
.split(' ')
.map((part) => part.charAt(0))
.join('')
.toUpperCase()
)
</script>
<template>
<article>
<p>{{ initials }}</p>
<h4>{{ name }}</h4>
<p>{{ email }}</p>
<p v-if="unreadCount">{{ unreadCount }} unread</p>
</article>
</template>
<style scoped>
article {
border: 1px solid #d0d0d0;
padding: 1rem;
}
</style>When to use it
- Render the same card component for every user in a list.
- Make a button component accept a label and a disabled flag.
- Size or theme a component from the page that uses it, without editing the component.
More examples
Passing the props from a parent
All three forms pass identical props. Note :unread-count="3" rather than unread-count="3": with the colon the value is a JavaScript expression and arrives as the number 3, and without it the value is a plain attribute string and arrives as "3", which fails type checking. The same-name shorthand on the third card only works when the local variable and the prop share a name, and it arrived in Vue 3.4.
<!-- src/views/TeamPage.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import UserCard from '../components/UserCard.vue'
const name = ref('Ada Lovelace')
const email = ref('[email protected]')
</script>
<template>
<!-- longhand -->
<user-card
v-bind:name="name"
v-bind:email="email"
v-bind:unread-count="3"
/>
<!-- the same three bindings, shorthand -->
<user-card :name="name" :email="email" :unread-count="3" />
<!-- same-name shorthand, Vue 3.4+: :name means :name="name" -->
<user-card :name :email :unread-count="3" />
</template>Default values, two ways
Two ways to give an optional prop a default. The destructuring form shown here uses ordinary JavaScript defaults, and from Vue 3.5 the destructured names stay reactive because the compiler rewrites them back to props.tone. The longhand is withDefaults(defineProps<Props>(), { tone: 'neutral' }), which does the same thing and is what you must use on Vue 3.4 and earlier. If you pass a destructured prop to a function that tracks it, wrap it in a getter: watch(() => tone, handler), not watch(tone, handler).
<!-- src/components/StatusBadge.vue -->
<script setup lang="ts">
// Destructured defaults. Destructured props stay reactive from Vue 3.5.
const { label, tone = 'neutral' } = defineProps<{
label: string
tone?: 'neutral' | 'warning'
}>()
</script>
<template>
<span :data-tone="tone">{{ label }}</span>
</template>
<style scoped>
span[data-tone='warning'] {
color: #8a4b00;
}
</style>Deriving from a prop instead of writing to it
The two patterns that replace writing to a prop. ref(props.initialText) copies the value at setup time, so the component owns something editable; note that later changes to the prop will not reach it, which is why the prop is named initialText. computed derives a value that does follow its source. Reach for the ref copy for editable form fields, and for computed whenever you only need the prop in a different shape.
<!-- src/components/DraftEditor.vue -->
<script setup lang="ts">
import { computed, ref } from 'vue'
const props = defineProps<{
initialText: string
}>()
// A local copy the component may change. The prop seeds it once.
const text = ref(props.initialText)
// A derived value. Recomputes whenever text changes.
const remaining = computed(() => 280 - text.value.length)
</script>
<template>
<div>
<textarea v-model="text"></textarea>
<p>{{ remaining }} characters left</p>
</div>
</template>
<style scoped>
textarea {
width: 100%;
}
</style>
Discussion