In Vue 3, why does a native custom element like `<color-swatch>` trigger "Failed to resolve component", and how does `compilerOptions.isCustomElement` fix it?
answer
- unknown tag looks like a component
- resolution runs, finds nothing, warns
- a predicate over tag names
- compile-time option, set in the build
basics
~20 sVue 3's compiler treats any non-native tag as a component and warns when none is registered. compilerOptions.isCustomElement, a tag-name predicate, makes it emit a plain element instead; with a build step it is set in the SFC build plugin.
solid answer
~40 sWhen Vue compiles a template, a tag that is not a known HTML or SVG element is assumed to be a Vue component and resolved against the registered components. A native custom element is not registered, so in development Vue warns `Failed to resolve component: color-swatch`, with a hint pointing at `compilerOptions.isCustomElement`, and then falls back to rendering the tag as an element. Setting `isCustomElement: (tag) => tag.startsWith('color-')` makes the compiler skip resolution entirely and emit a plain element. It is a **compile-time** option: with a build step it goes in the SFC build plugin's template `compilerOptions`; `app.config.compilerOptions` is only honoured when templates compile in the browser. Keep the predicate narrow — `tag.includes('-')` also swallows kebab-case Vue components such as `<base-button>`.
code
ts · 11 lines// SFC build plugin options (the template compiler runs at build time)
const vueOptions = {
template: {
compilerOptions: {
isCustomElement: (tag: string) => tag.startsWith('color-'),
},
},
}
// Only for templates compiled in the browser with the full build:
// app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('color-')go deeper
Recognise the Failed to resolve component warning on a web-component tag and know that isCustomElement tells Vue not to treat that tag as a component.
Explain that the option is read by the template compiler, so a build-step project sets it in the SFC plugin rather than on app.config.
Choose a predicate that cannot capture your own kebab-case components, and keep the warning log clean so real resolution failures stay visible.
Standardise how third-party element prefixes are declared across projects, so adopting a new web-component library is one config change, not a hunt.
## Two kinds of unfamiliar tag A Vue template can contain three kinds of tag: **native elements** (`div`, `svg`), **Vue components** (`MyButton`, `my-button`) and **custom elements** — elements defined with the browser's own custom elements API, often by a design-system library that has nothing to do with Vue. The compiler has to decide, for every tag, whether to emit a plain element or a component lookup. It knows the list of native HTML and SVG tags, so anything outside that list is, by default, **assumed to be a Vue component**. ## What happens without configuration For an unknown tag such as `<color-swatch>`, the compiled render function calls `resolveComponent('color-swatch')`: 1. Vue looks for a component with that name among the current component's registrations and the app's global registrations. 2. It finds none, so in development it logs `Failed to resolve component: color-swatch`, followed by a hint: if this is a native custom element, exclude it from component resolution via `compilerOptions.isCustomElement`. 3. `resolveComponent` then returns the tag name as a string, and the renderer creates a plain element with that name. So the element still renders. The costs are a lookup on every render, a console full of warnings that hide real ones, and code that relies on a fallback instead of saying what it means. ## The fix: `isCustomElement` `isCustomElement` is a function `(tag: string) => boolean`. The template compiler calls it for each tag **before** its component check; if it returns `true`, the tag is compiled as a plain element — no `resolveComponent` call, no warning. - `(tag) => tag.startsWith('color-')` — matches one library's prefix. Safest. - `(tag) => tag.includes('-')` — matches every hyphenated tag. Common in guides, but it also catches kebab-case Vue components. - `(tag) => knownElements.has(tag)` — an explicit list, most precise. ## Where the option goes This is the part people get wrong. Templates are usually compiled **ahead of time** by the SFC build plugin, and the runtime shipped to the browser contains no compiler. So: | Setup | Where `isCustomElement` goes | Effect of `app.config.compilerOptions` | |---|---|---| | SFCs compiled by a build step | the SFC plugin's `template.compilerOptions` | ignored; a dev warning says it is only respected by the full build | | Templates compiled in the browser (full build) | `app.config.compilerOptions.isCustomElement` | applied at runtime compile | If you set it on `app.config` in a normal build-step project, nothing changes except a new warning explaining that `compilerOptions` must be passed to the build setup instead. The old top-level `app.config.isCustomElement` is deprecated in favour of `compilerOptions.isCustomElement`. ## The kebab-case trap Because the predicate is checked **first**, a broad rule wins over your own components. With `tag.includes('-')`: - `<BaseButton>` still compiles as a component (the tag has no hyphen); - `<base-button>` compiles as a plain, empty `base-button` element — no warning, because resolution was skipped. Teams that write kebab-case component tags should match a library prefix instead of every hyphen. ## What the option does not do - It does not **define** the element; the library's script must still call `customElements.define` before the element can upgrade. - It does not change how bindings are applied: Vue still decides per binding whether to set a DOM property or an attribute. - It is not only for third-party elements: a custom element you built with Vue's own `defineCustomElement` is, to another Vue app's templates, just another native tag, so that consuming app needs the predicate too. ## Diagnosing it 1. Read the warning's tag name: it tells you exactly which tag the compiler treated as a component. 2. Check whether a component with that name was meant to exist — a typo in a registered component's name produces the same warning, and a broad predicate would hide that real bug. 3. If the tag is a genuine custom element, add it (or its prefix) to the predicate in the build plugin, restart the dev server so the compiler picks it up, and confirm the warning is gone. In an interview, the crisp version is: the warning comes from component resolution, the fix is a compile-time predicate, it lives in the build plugin's compiler options, and it should be as narrow as your naming allows.
- Why is `tag.includes('-')` risky as an `isCustomElement` predicate?The compiler checks `isCustomElement` before deciding a tag is a component, so any Vue component written in kebab-case in a template, such as `<base-button>`, is compiled as a plain unknown element and renders empty — silently, because resolution and its warning are skipped. PascalCase tags are unaffected. Matching a library prefix, or an explicit set of tag names, avoids the collision.
- Why does setting `app.config.compilerOptions.isCustomElement` in `main.ts` not silence the warning in a typical SFC project?SFC templates are compiled during the build, and the runtime-only build shipped to the browser has no template compiler to read runtime compiler options. Vue says so with a development warning that `compilerOptions` is only respected by the full build. The predicate has to be passed to the SFC build plugin's template compiler options.
saying these in an interview costs you the question
- Vue refuses to render an unknown custom element until it is registered as a component.
- Setting app.config.compilerOptions.isCustomElement works the same in every build.
- isCustomElement registers the custom element with the browser.
- tag.includes('-') is always a safe predicate.
- The warning means the custom element's script failed to load.