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:

  1. Use defineEmits with TypeScript for full type safety

    You’re explicitly declaring what kind of event you emit and what payload it expects.

  2. Use @click.stop in the template to prevent default bubbling

    Then re-emit manually using emit('click', e) to allow parent components to handle it.

  3. 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!