skip to content

In Vue 3, when does a template tag like `<UserCard>` or `<user-card>` resolve to a component, and why do in-DOM templates force kebab-case?

level: middleimportance: nice to knowfreq 35%

answer

  1. how registered names are matched
  2. one direction only
  3. browsers lowercase HTML
  4. no self-closing custom tags in HTML

basics

~20 s

Vue matches a tag against registered names as written, camelized and capitalized, so a PascalCase registration answers to both <UserCard> and <user-card>. In-DOM templates are parsed by the browser first, which lowercases tags and ignores self-closing custom tags, so they need kebab-case and closing tags.

solid answer

~40 s

When Vue resolves a tag against its registry, it tries the tag as written, then camelized, then capitalized. So a component registered as `UserCard` (PascalCase, the recommended form) matches both `<UserCard>` and `<user-card>`. The reverse does not hold: a component registered under `'user-card'` matches only the kebab-case tag, because `UserCard` never converts back to kebab-case. In SFCs and string templates, Vue's own parser reads the template, so `<UserCard />` works and is preferred because it stands out from native elements and custom elements. **In-DOM templates** (markup written in the page and mounted without a build step) are parsed by the browser first: HTML is case-insensitive, so `<UserCard>` arrives as `usercard`, and a self-closing `<user-card />` is not closed. There you must write `<user-card></user-card>`, and camelCase props and events in kebab-case too.

code

ts · 11 lines
ts
import { createApp } from 'vue'
import UserCard from './UserCard.vue'
import StatusPill from './StatusPill.vue'

const app = createApp({})

// matches <UserCard> and <user-card>
app.component('UserCard', UserCard)

// matches <status-pill> only; <StatusPill> fails to resolve
app.component('status-pill', StatusPill)

go deeper

for a junior

Remember the recommended PascalCase tag in SFCs and that in-DOM templates need kebab-case tags with explicit closing tags.

for a middle

Explain the three spellings Vue tries and why that makes a PascalCase registration work for both tag styles while a kebab-case one does not.

for a senior

Diagnose a component that resolves in SFCs but not in server-rendered in-DOM markup, and set naming rules so both template sources keep working.

for a principal

Decide whether a product that embeds Vue into existing server-rendered pages should rely on in-DOM templates at all, given the casing and parsing constraints.

## How Vue matches a tag to a registered name A Vue template tag that is not a native element or a built-in is looked up in the component registries: first the component's local registrations, then the app's global ones. For each registry, Vue tries three spellings of the **tag**: 1. the tag exactly as written; 2. the tag **camelized** (`user-card` becomes `userCard`); 3. the camelized tag **capitalized** (`userCard` becomes `UserCard`). The registry keys are never transformed. That makes the matching one-directional: | Registered as | `<UserCard>` | `<user-card>` | |---|---|---| | `UserCard` | Matches | Matches (capitalized camel form) | | `userCard` | No (tried as-is and capitalized only) | Matches (camelized form) | | `user-card` | No | Matches (as written) | This is why the Vue docs recommend registering and importing components in **PascalCase**: one name then serves every template style. ## Why PascalCase in SFCs In a single-file component or a string template, the template is parsed by Vue's own compiler, which is case-sensitive and understands `/>` on any tag. The recommended style is `<UserCard />` because: - PascalCase names are valid JavaScript identifiers, so the import name and the tag are the same word, and editors can auto-complete and navigate them; - a PascalCase tag is visibly a Vue component rather than a native element; - it also distinguishes Vue components from **custom elements** (web components), whose names must contain a hyphen. In `<script setup>`, the compiler binds `<UserCard>` directly to the imported `UserCard` variable, and it also accepts `<user-card>` for the same import. ## In-DOM templates are parsed by the browser first An **in-DOM template** is markup written directly in an HTML page, which Vue reads from the DOM and compiles at runtime. Before Vue sees it, the browser's HTML parser has already processed it, with two consequences: - **Case is lost.** HTML tag and attribute names are case-insensitive, so `<UserCard :userId="id">` reaches Vue as `usercard` with an attribute named `userid`. Component tags, camelCase props and `v-on` event names must all be written in kebab-case: `<user-card :user-id="id" @update-user="save">`. - **Self-closing custom tags do not close.** HTML allows only void elements such as `<input>` and `<img>` to omit a closing tag. `<user-card />` followed by `<span>` is parsed as a `user-card` element that *contains* the span. Always write an explicit closing tag. None of this applies to SFCs, inlined `template: '...'` strings, or `<script type="text/x-template">` sources, which Vue parses itself. ## Practical rules - Register and import components in PascalCase. - In SFCs and string templates, write `<UserCard />`. - In in-DOM templates, write `<user-card></user-card>` and kebab-case every prop and event. - Avoid registering a component under a native tag name such as `button` or a built-in such as `slot`; Vue warns in development: `Do not use built-in or reserved HTML elements as component id`. ## Example ```html <div id="app"> <user-card :user-id="42" @update-user="onUpdate"></user-card> </div> ``` This in-DOM markup resolves to a component registered as `UserCard`, provided the application uses a build of Vue that includes the runtime template compiler.

  • A Vue 3 in-DOM template writes `<user-card />` followed by `<footer-bar></footer-bar>`, and the footer appears inside the card. Why?
    The browser parses in-DOM templates before Vue does, and HTML only lets void elements such as `<input>` self-close. The `/>` on `user-card` is ignored, so the element stays open and swallows the following markup as its child. Write `<user-card></user-card>`.
  • In a Vue 3 in-DOM template, a component declares a `userId` prop. How must the parent write the binding?
    As `:user-id="..."`. The browser lowercases attribute names, so `:userId` would reach Vue as `userid` and match no prop. Vue maps kebab-case attributes to camelCase prop names, so the kebab form works in every template style.

saying these in an interview costs you the question

  • A component registered as 'user-card' can also be used as <UserCard>.
  • Self-closing component tags work in any Vue template, including in-DOM ones.
  • PascalCase tags work in in-DOM templates because Vue reads the original source.
  • Kebab-case tags only work if the component was registered in kebab-case.