Props carry data down the component tree; emitted events carry information back up. Together they form Vue's one-way data flow: the parent owns state, the child requests changes. In this lesson you will emit custom events, pass payloads, and build components that support v-model -- the pattern behind every reusable form control.
Declare events with the defineEmits macro, then call the returned function:
<!-- AddToCartButton.vue -->
<script setup>
defineProps({ productId: Number })
const emit = defineEmits(['add-to-cart'])
</script>
<template>
<button @click="emit('add-to-cart', productId, 1)">
Add to cart
</button>
</template>The parent listens with the same @ syntax it uses for DOM events:
<script setup>
import AddToCartButton from './AddToCartButton.vue'
function handleAdd(id, qty) {
console.log('Add product', id, 'x', qty)
}
</script>
<template>
<AddToCartButton :product-id="42" @add-to-cart="handleAdd" />
</template>Everything after the event name in emit(...) becomes arguments to the listener. Event names are written kebab-case in templates, matching prop conventions.
Like props, emits support object syntax with validators (dev-mode warnings only):
const emit = defineEmits({
'add-to-cart': (id, qty) => Number.isInteger(id) && qty > 0
})If a child silently changed parent data, you could never tell where a value was modified. Emits make every state change traceable: search the parent for @event-name and you find the single function responsible. Vue Devtools even logs every emitted event with its payload.
On a native input, v-model="text" is sugar for :value="text" plus @input. On a component, Vue 3.4+ gives you the elegant defineModel macro:
<!-- SearchInput.vue -->
<script setup>
const model = defineModel() // a ref synced with the parent
</script>
<template>
<input :value="model" @input="model = $event.target.value" class="search">
</template>The parent simply writes:
<SearchInput v-model="query" />Reading model gives the parent's value; assigning to it emits the update automatically. Under the hood this is still a modelValue prop plus an update:modelValue event -- the pre-3.4 pattern you may meet in older codebases:
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
// emit('update:modelValue', newValue)A component can expose several two-way bindings:
<!-- UserForm.vue -->
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script><UserForm v-model:first-name="first" v-model:last-name="last" /><!-- StarRating.vue -->
<script setup>
const rating = defineModel({ type: Number, default: 0 })
const stars = [1, 2, 3, 4, 5]
</script>
<template>
<span v-for="s in stars" :key="s" @click="rating = s"
:style="{ cursor: 'pointer', color: s <= rating ? 'gold' : '#ccc' }">
★
</span>
</template>Any parent can now write <StarRating v-model="review.score" /> and the score stays perfectly in sync -- a genuinely reusable form control in a dozen lines.
defineEmits and emit('event', ...payload).@event-name, receiving the payload as arguments.defineModel() (Vue 3.4+) makes custom components work with v-model in one line.v-model:first-name) allow multiple two-way bindings per component.Next up: Slots and Content Distribution -- passing template content, not just data, into components.