Files
stack/packages/mosaic/framework/skills/vue-best-practices/reference/emit-kebab-case-in-templates.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05:00

5.0 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Use kebab-case for Event Listeners in Templates LOW Vue auto-converts camelCase emits to kebab-case listeners but consistency improves readability best-practice
vue3
events
emit
naming-convention
templates

Use kebab-case for Event Listeners in Templates

Impact: LOW - Vue automatically converts event names between camelCase and kebab-case. You can emit in camelCase (emit('someEvent')) and listen with kebab-case (@some-event). However, following consistent conventions improves code readability and matches HTML attribute conventions.

Task Checklist

  • Emit events using camelCase in JavaScript: emit('updateValue')
  • Listen to events using kebab-case in templates: @update-value
  • Be consistent across your codebase
  • Understand Vue's automatic case conversion

The Convention

Recommended pattern:

<!-- ChildComponent.vue -->
<script setup>
const emit = defineEmits(['updateValue', 'itemSelected', 'formSubmit']);

function handleChange(value) {
  // Emit in camelCase (JavaScript convention)
  emit('updateValue', value);
}

function selectItem(item) {
  emit('itemSelected', item);
}
</script>
<!-- ParentComponent.vue -->
<template>
  <!-- Listen in kebab-case (HTML attribute convention) -->
  <ChildComponent
    @update-value="handleUpdate"
    @item-selected="handleSelect"
    @form-submit="handleSubmit"
  />
</template>

Vue's Automatic Conversion

Vue handles these automatically in template syntax only:

Emitted (camelCase) Listener (kebab-case) Works?
emit('updateValue') @update-value Yes
emit('itemSelected') @item-selected Yes
emit('formSubmit') @form-submit Yes
<!-- All of these work equivalently in Vue 3 templates -->
<Child @updateValue="handler" />
<!-- camelCase listener -->
<Child @update-value="handler" />
<!-- kebab-case listener (preferred) -->

Important: Template-Only Behavior

This auto-conversion only works in template syntax (@event-name). It does NOT work in render functions or programmatic event listeners:

// In render functions, use camelCase with 'on' prefix
import { h } from 'vue';

// CORRECT - camelCase event name with 'on' prefix
h(ChildComponent, {
  onUpdateValue: (value) => handleUpdate(value),
  onItemSelected: (item) => handleSelect(item),
});

// WRONG - kebab-case does NOT work in render functions
h(ChildComponent, {
  'onUpdate-value': (value) => handleUpdate(value), // Won't work!
  'on-update-value': (value) => handleUpdate(value), // Won't work!
});
// Programmatic listeners also require camelCase
import { ref, onMounted } from 'vue';

const childRef = ref<ComponentPublicInstance | null>(null);

onMounted(() => {
  // CORRECT - camelCase
  childRef.value?.$on?.('updateValue', handler);

  // WRONG - kebab-case won't match
  childRef.value?.$on?.('update-value', handler); // Won't work!
});

Summary:

  • Templates: Auto-conversion works (@update-value matches emit('updateValue'))
  • Render functions: Must use onEventName format (camelCase with on prefix)
  • Programmatic listeners: Must use the exact emitted event name (typically camelCase)

Why kebab-case in Templates?

  1. HTML convention: HTML attributes are case-insensitive and traditionally kebab-case
  2. Consistency with props: Props follow the same pattern (props.userName -> user-name="...")
  3. Readability: @user-profile-updated is easier to read than @userProfileUpdated
  4. Vue style guide: Vue's official style guide recommends this pattern

TypeScript Declarations

When using TypeScript, define emits in camelCase:

<script setup lang="ts">
const emit = defineEmits<{
  updateValue: [value: string]; // camelCase
  itemSelected: [item: Item]; // camelCase
  'update:modelValue': [value: string]; // Special v-model syntax (with colon)
}>();
</script>

v-model Events

For v-model, the update: prefix uses a colon, not kebab-case:

<script setup>
// Correct: colon separator for v-model updates
const emit = defineEmits(['update:modelValue', 'update:firstName']);

function updateValue(newValue) {
  emit('update:modelValue', newValue);
}
</script>
<!-- Parent - v-model handles the event automatically -->
<CustomInput v-model="value" />
<CustomInput v-model:first-name="firstName" />

Vue 2 Difference

In Vue 2, event names did NOT have automatic case conversion. This caused issues:

// Vue 2 - camelCase events couldn't be listened to in templates
this.$emit('updateValue') // Emitted as 'updateValue'

// Template converts to lowercase
<child @updatevalue="handler">  // Listened as 'updatevalue' - NO MATCH!

Vue 3 fixed this with automatic camelCase-to-kebab-case conversion.

Reference