Why is this an issue?

In Vue 3, an event that isn’t declared in the component’s emits option isn’t treated as a component event: Vue instead attaches the parent’s v-on listener directly to the component’s root element as a plain DOM listener.

This is most dangerous when the event name collides with a native DOM event, such as click, submit, or change: the parent’s handler then fires twice, once from $emit() with the component’s own payload and once from the real DOM event, and a handler written for the custom payload can throw on that second call.

Even a name that doesn’t collide with anything today isn’t safe: the DOM listener is still registered under that name and can start firing later, if the root element changes or a third party dispatches an event with the same name. In the common single-root case, nothing warns you when this happens, which is why this rule flags it.

Declaring the event also gives the type checker something to check the event name against: without a declaration, a typo like this.$emit('svae') fails silently, with nothing warning you.

How to fix it

Add the event name to the component’s emit declaration. How you do this depends on the component’s API style: the emits option in the Options API and in setup(), or defineEmits inside <script setup>, which is not available anywhere else.

Code examples

Noncompliant code example

<script>
export default {
  methods: {
    onSubmit() {
      this.$emit('submit'); // Noncompliant: "submit" is not declared in emits
    }
  }
}
</script>

Compliant solution

<script>
export default {
  emits: ['submit'],
  methods: {
    onSubmit() {
      this.$emit('submit');
    }
  }
}
</script>

With Composition API’s explicit setup(props, { emit }), the event still needs to be declared in the component’s emits option, not through defineEmits, since defineEmits only exists inside <script setup>. A quickfix is available for both this and the Options API case above.

Noncompliant code example

export default {
  setup(props, { emit }) {
    function onSubmit() {
      emit('submit'); // Noncompliant: "submit" is not declared in emits
    }
    return { onSubmit };
  }
}

Compliant solution

export default {
  emits: ['submit'],
  setup(props, { emit }) {
    function onSubmit() {
      emit('submit');
    }
    return { onSubmit };
  }
}

<script setup>, Composition API’s compile-time sugar, has no this, so $emit is not available in its script block (it still works directly in template expressions). Declare your events with const emit = defineEmits([…​]), and call them through the emit function it returns. A quickfix can add a missing event name to an existing defineEmits array, but there is no quickfix to create the declaration from scratch.

Noncompliant code example

<script setup>
const emit = defineEmits(['submit']);

function onSubmit() {
  emit('submit');
}

function onCancel() {
  emit('cancel'); // Noncompliant: "cancel" is not declared in defineEmits
}
</script>

Compliant solution

<script setup>
const emit = defineEmits(['submit', 'cancel']);

function onSubmit() {
  emit('submit');
}

function onCancel() {
  emit('cancel');
}
</script>

The standard form since Vue 3.3 is the tuple syntax, defineEmits<{ submit: [] }>(). Unlike the plain JavaScript array, adding an event name here is not risk-free: writing cancel: [] for an event that actually carries a payload creates a new TypeScript error, so the payload type has to match what is emitted. If the payload type is not known yet, or is not worth typing precisely, a fallback works too, from best to worst: [payload: SomeType], [payload: unknown], or […​args: any[]]. There is no quickfix for any type-based declaration, and eslint-plugin-vue does not offer a suggestion for this form either.

Noncompliant code example

<script setup lang="ts">
const emit = defineEmits<{ submit: [] }>();

function onSubmit() {
  emit('submit');
}

function onCancel() {
  emit('cancel'); // Noncompliant: "cancel" is not declared in defineEmits
}
</script>

Compliant solution

<script setup lang="ts">
const emit = defineEmits<{ submit: []; cancel: [] }>();

function onSubmit() {
  emit('submit');
}

function onCancel() {
  emit('cancel');
}
</script>

Pitfalls

Declaring an event whose name matches a native DOM event stops the parent’s listener from being attached as a native listener on the root element. Before you declare such an event, make sure the component actually emits it on every interaction where consumers expect the native event to fire. This also affects components that check $attrs directly, such as v-if="$attrs.onClose": they stop seeing the listener as soon as it is declared.

For submit specifically, .prevent calls preventDefault() on whatever the child emits, so this.$emit('submit') with no payload throws a TypeError once the event is declared. Emit the native event as the payload (this.$emit('submit', event)) to avoid this.

Resources

Documentation