Files
agent-skills/skills/vue-best-practices/reference/emit-kebab-case-in-templates.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06: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