Custom events allow child components to communicate with their parents. In Vue 3, we use defineEmits() to declare and type our events.
Basic Event Emission
vue<script setup lang="ts"> const emit = defineEmits<{ submit: [] cancel: [] }>() function handleSubmit() { // Perform submission logic... emit('submit') } function handleCancel() { emit('cancel') } </script> <template> <div class="form-actions"> <button @click="handleCancel">Cancel</button> <button @click="handleSubmit">Submit</button> </div> </template>
Events with Payloads
Pass data with your events:
vue<script setup lang="ts"> type User = { id: number name: string email: string } const emit = defineEmits<{ select: [user: User] delete: [userId: number] search: [query: string, filters: string[]] }>() function handleUserSelect(user: User) { emit('select', user) } function handleDelete(userId: number) { if (confirm('Are you sure?')) { emit('delete', userId) } } function handleSearch(query: string) { emit('search', query, ['active', 'recent']) } </script>
Runtime Declaration
Alternative syntax without TypeScript:
vue<script setup> const emit = defineEmits({ // No validation click: null, // With validation submit: (payload) => { if (payload.email && payload.password) { return true } else { console.warn('Invalid submit payload') return false } }, // Simple validation increment: (amount) => typeof amount === 'number' }) </script>
Listening to Events
Parent components use @event-name:
vue<script setup lang="ts"> import UserCard from './UserCard.vue' function handleSelect(user) { console.log('Selected:', user) } function handleDelete(userId) { console.log('Deleting:', userId) } </script> <template> <UserCard @select="handleSelect" @delete="handleDelete" /> </template>
Inline Handlers
For simple cases:
vue<template> <!-- Access payload with $event --> <SearchBox @search="searchQuery = $event" /> <!-- Arrow function for multiple args --> <SearchBox @search="(query, filters) => performSearch(query, filters)" /> <!-- Direct method reference (payload auto-passed) --> <UserCard @select="selectUser" /> </template>
Event Naming Convention
Use camelCase in defineEmits, kebab-case in templates:
vue<script setup> const emit = defineEmits({ updateValue: null, // camelCase in JS itemSelected: null, formSubmitted: null }) emit('updateValue', newValue) emit('itemSelected', item) </script>
vue<!-- Parent template uses kebab-case --> <MyComponent @update-value="handleUpdate" @item-selected="handleSelect" @form-submitted="handleSubmit" />
Event Validation in Development
Validated events warn during development:
vue<script setup> const emit = defineEmits({ submit: (payload) => { if (!payload.email) { console.warn('submit event: missing email') return false } if (!payload.password || payload.password.length < 8) { console.warn('submit event: invalid password') return false } return true } }) // This will warn in development emit('submit', { email: 'test@test.com' }) // Missing password! </script>
Best Practices
- Always declare events with
defineEmits - Use descriptive names that describe what happened
- Validate payloads for complex data
- Document expected payloads with TypeScript types
- Keep payloads simple - avoid deeply nested objects