Component Events and v-model
|
This section documents the current Vue 3.x release line as published at
the official Vue.js documentation, which is the reference these pages are written and
verified against. No specific patch version is pinned. The Composition API with This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
A child talks back to its parent by emitting an event; the parent listens with v-on (@). v-model is
built directly on this mechanism, and defineModel() packages the whole round trip into one writable ref.
This page also covers fallthrough attributes — what becomes of attributes and listeners the parent puts on
a component tag that the child never declared.
Emitting events
In a template the compiler exposes $emit directly:
<template>
<button @click="$emit('submit')">OK</button>
</template>
In script, get the emit function from the defineEmits() macro (compile-time, <script setup> only, no
import):
<script setup>
const emit = defineEmits(['submit', 'cancel'])
function save() {
// ...do work
emit('submit')
}
</script>
See Component Events.
Declaring emitted events
Declaring events is optional but recommended: it documents the component’s interface and changes fallthrough
behaviour — a declared event’s listener is not added to $attrs, so it is not applied to the root element.
Runtime declaration:
const emit = defineEmits(['change', 'delete'])
Type-based declaration — a call signature per event, giving each payload a static type:
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'delete', id: number, soft: boolean): void
}>()
Since 3.3 the shorter labelled-tuple form is also accepted:
const emit = defineEmits<{
change: [id: number]
delete: [id: number, soft: boolean]
}>()
Event validation
The object form of defineEmits() maps each name to a validator that receives the emitted arguments and
returns a boolean; false logs a development-only warning:
const emit = defineEmits({
// no validation
click: null,
// validate the payload
submit: ({ email, password }) => {
if (!email || !password) {
console.warn('submit payload missing fields')
return false
}
return true
}
})
Event names, arguments, and casing
-
Emit with the exact string you declared — Vue does not transform event names, so an event emitted as
myEventcannot be heard asmy-event. The reliable convention is kebab-case in both the declaration and the template listener:emit('update-count')paired with@update-count. -
Any arguments after the name are forwarded to the listener:
emit('resize', width, height)calls the handler asonResize(width, height).
See Event Arguments.
Events vs. callback props
Vue can also pass behaviour down as a function prop (:on-select="handleSelect"), the way React does. The
official guidance is to prefer emitted events: they work with v-on modifiers, show up in the devtools
timeline, and are declared in one place. Reserve function props for the case where the child must call back
synchronously and use the return value. See
Events vs. Callback Props.
How events power v-model
v-model on a component is sugar for a prop plus its update event. Since Vue 3.4 an argument targets the
name + update:name pair, and a bare v-model uses modelValue + update:modelValue:
<!-- these two are equivalent -->
<SearchBox v-model="query" />
<SearchBox :model-value="query" @update:model-value="query = $event" />
Because v-model:title targets an independent pair, a component can carry several v-model bindings at once.
defineModel()
defineModel() (stable since 3.4) implements the child side of v-model. It declares the prop and its update
event together and returns a writable ref: reading it reads the prop, assigning to it emits the update.
<script setup>
const model = defineModel()
</script>
<template>
<input :value="model" @input="model = $event.target.value" />
</template>
An argument names the bound key, matching v-model:<name> on the parent:
<script setup>
const title = defineModel('title')
const done = defineModel('done')
</script>
<!-- parent: <TodoItem v-model:title="t" v-model:done="d" /> -->
Options follow the name (or come first when there is none) and mirror defineProps options plus a get /
set transformer:
const model = defineModel<string>({ required: true })
const count = defineModel<number>('count', {
default: 0,
set: (value) => Math.max(0, value) // clamp on every write
})
Multiple v-model bindings are just multiple defineModel() calls with different names, as above.
Custom modifiers. Destructure the returned pair to read modifiers the parent attached
(v-model.capitalize="draft"), and use a set transformer to act on them:
<script setup>
const [model, modifiers] = defineModel({
set(value) {
return modifiers.capitalize
? value.charAt(0).toUpperCase() + value.slice(1)
: value
}
})
</script>
See Component v-model and
defineModel().
The pre-3.4 equivalent
Before defineModel() the child wired the prop and event by hand. This still works and is, conceptually, what
defineModel() compiles to:
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function onInput(e) {
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="props.modelValue" @input="onInput" />
</template>
Fallthrough attributes
An attribute or v-on listener the parent puts on a component tag, but which the child declared as neither a
prop nor an emit, is a fallthrough attribute. By default Vue applies these to the child’s single root
element.
<!-- Child.vue -->
<template>
<button class="btn"><slot /></button>
</template>
<!-- Parent -->
<Child class="primary" id="save" @focus="onFocus" />
<!-- rendered root: <button class="btn primary" id="save"> plus the focus listener -->
-
classandstyleare merged with whatever the root element already has. -
v-onlisteners are merged too: the child’s own@focusand the parent’s both run. -
Everything else (
id,data-,aria-, …) is added, with the parent’s value winning on a clash.
Disabling and redirecting with $attrs
Set inheritAttrs: false to stop the automatic application, then bind $attrs where you actually want it — usually an inner element rather than the wrapper:
<script setup>
defineOptions({ inheritAttrs: false })
</script>
<template>
<div class="field">
<label><slot /></label>
<input v-bind="$attrs" />
</div>
</template>
In <script setup> $attrs is available in the template directly; in script, read it with useAttrs():
import { useAttrs } from 'vue'
const attrs = useAttrs() // non-reactive object; read it at call time
See also
-
Registration and Props — the prop side of the component interface.
-
Slots — passing template content rather than data or events.
-
React Reference and Angular Reference — callback props and
output()as the same idea elsewhere.