When building reusable components in Vue 3, it's common to forward native DOM events like click
to the parent. But did you know that if you're not careful, you could accidentally suppress the parent's event listener?
Let’s explore how to correctly emit a native click
event while preserving expected behavior in parent components—with full type safety using TypeScript.
Suppose you're building a custom <ClickableCard>
component that should behave like a button or clickable div. You want consumers of the component to do this:
<ClickableCard @click="doSomething" />
Easy, right? Just listen to a click inside and emit it:
const emit = defineEmits(['click']);
const handleClick = (e: MouseEvent) => emit('click', e);
But here’s the catch: Vue treats @click
as a native DOM listener, unless you manually emit a click
event. If your component listens for @click
itself, that overrides the native bubbling behavior. This means your component must explicitly re-emit the click for it to propagate.
Here’s how you do it right:
<script setup lang="ts">
import { defineEmits, computed, getCurrentInstance } from 'vue';
const props = defineProps<{
disabled?: boolean;
}>();
// Define the 'click' event and its payload type
const emit = defineEmits<{
(e: 'click', event: MouseEvent): void;
}>();
// Check if parent has registered a click listener
const hasClickEventListener = computed(() =>
!!getCurrentInstance()?.vnode.props?.onClick
);
const handleClick = (e: MouseEvent): void => {
if (props.disabled) {
return;
}
emit('click', e); // Explicitly emit
};
</script>
<template>
<div @click.stop="handleClick">
<!-- Your content here -->
<slot />
</div>
</template>
Key takeaways:
-
Use
defineEmits
with TypeScript for full type safetyYou’re explicitly declaring what kind of event you emit and what payload it expects.
-
Use
@click.stop
in the template to prevent default bubblingThen re-emit manually using
emit('click', e)
to allow parent components to handle it. -
Check for click listeners
The
hasClickEventListener
computed property lets you conditionally style the component as “clickable” only when a parent listens for the event.const hasClickEventListener = computed(() => !!getCurrentInstance()?.vnode.props?.onClick );
Vue 3 no longer automatically forwards native DOM events from custom components like it did in Vue 2. So if you want to create reusable, button-like components, manually re-emitting native events is essential to preserving intuitive parent usage.
More on this in the Vue docs: Declaring Emitted Events
When building components in Vue 3 with the Composition API and TypeScript, always remember:
- Native DOM events need to be manually re-emitted.
- Use
defineEmits
for strong typing. - Use
.stop
to prevent double firing or propagation when necessary. - Check for registered listeners to apply behavior conditionally.
This pattern ensures your components behave predictably, are type-safe, and stay developer-friendly!
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.