Sending Events Out
Declare events with defineEmits, emit them with a payload, and keep data flowing one way: props down, events up.
const emit = defineEmits<{ change: [value: number] }>() → emit('change', 1)A prop carries data into a component. An event carries news back out. Together they keep data moving in one direction: the parent owns the state, the child reports what happened, and the parent decides what changes. That is what makes a component predictable, because nothing reaches upward and edits state it does not own.
Declaring and emitting
Declare your events with defineEmits, another compile-time macro that needs no import. It returns an emit function. Call it with the event name and any payload arguments; every argument is forwarded to the listener. In TypeScript, since Vue 3.3, you declare each event as a named tuple of its payload: change: [quantity: number]. The older call-signature form, (e: 'change', quantity: number): void, means the same and still works.
In the template you can skip the macro and write $emit('change', 1) directly. $emit and the returned emit are the same function.
Names
Emit in camelCase and listen in kebab-case: emit('increaseBy', 1) is heard as @increase-by. A listener can name a function, @change="onChange", which receives the payload as its argument, or run an inline expression where $event is that payload: @change="quantity = $event".
What trips people up
Component events do not bubble, so only the direct parent hears them. And declaring a native name such as click in defineEmits stops the native click from falling through, so the parent's @click then fires only when you emit.
Example
<!-- src/components/QuantityStepper.vue -->
<script setup lang="ts">
const props = defineProps<{
quantity: number
max: number
}>()
const emit = defineEmits<{
change: [quantity: number]
limit: []
}>()
function step(by: number): void {
const next = props.quantity + by
if (next < 1) return
if (next > props.max) {
emit('limit')
return
}
emit('change', next)
}
</script>
<template>
<div>
<button type="button" @click="step(-1)">Remove one</button>
<span>{{ quantity }}</span>
<button type="button" @click="step(1)">Add one</button>
</div>
</template>
<style scoped>
div {
display: flex;
align-items: center;
gap: 0.5rem;
}
</style>When to use it
- Let a row component tell the page which item the user deleted.
- Report a validated form payload up to the page that will send it.
- Signal that a child hit a limit, so the page can show one message in one place.
More examples
The parent listening and owning the state
The page owns quantity; the stepper never touches it. @change="onChange" passes the emitted payload as the function's first argument, and @limit runs an inline expression because there is no payload to handle. Note the kebab-case listener names matching the camelCase emits. The whole round trip is visible in one file, which is why this shape is easy to debug: if the number is wrong, either the child emitted the wrong value or this handler mishandled it.
<!-- src/views/CartPage.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import QuantityStepper from '../components/QuantityStepper.vue'
const quantity = ref(1)
const notice = ref('')
function onChange(next: number): void {
quantity.value = next
notice.value = ''
}
</script>
<template>
<quantity-stepper
:quantity="quantity"
:max="5"
@change="onChange"
@limit="notice = 'Five is the maximum.'"
/>
<p v-if="notice">{{ notice }}</p>
</template>Emitting straight from the template
When nothing happens in script, you do not need the emit function. $emit in a template is the same function defineEmits returns, so this is shorthand for const emit = defineEmits(...) followed by @click="emit('dismiss', 'user')". Still call defineEmits for the declaration: it documents the component, gives the payload a type, and keeps the event out of the attributes that fall through to the root element.
<!-- src/components/DismissBanner.vue -->
<script setup lang="ts">
defineProps<{
message: string
}>()
defineEmits<{
dismiss: [reason: 'user' | 'timeout']
}>()
</script>
<template>
<aside role="status">
<p>{{ message }}</p>
<button type="button" @click="$emit('dismiss', 'user')">
Dismiss
</button>
</aside>
</template>
<style scoped>
aside {
display: flex;
gap: 1rem;
background: #eef3ff;
padding: 0.75rem;
}
</style>The older call-signature type form
The same two events written as overloaded call signatures, which is what you will see in code written before Vue 3.3 and in much published material. It behaves identically; the named-tuple form is shorter and easier to read, so prefer it in new code. This example also shows a form submit handler emitting: the component decides which event fits, and the parent decides what to do about it.
<!-- src/components/SearchBox.vue -->
<script setup lang="ts">
import { ref } from 'vue'
// Pre-3.3 form. Equivalent to:
// search: [term: string]
// clear: []
const emit = defineEmits<{
(e: 'search', term: string): void
(e: 'clear'): void
}>()
const term = ref('')
function submit(): void {
if (term.value === '') {
emit('clear')
return
}
emit('search', term.value)
}
</script>
<template>
<form @submit.prevent="submit">
<input v-model="term" type="search" />
<button type="submit">Search</button>
</form>
</template>
Discussion