The is attribute on <component> accepts a component definition object, a component name string, or an HTML tag name string. Vue resolves it at runtime and renders the matching component or element. Beyond the standard <component :is> pattern, is has special behavior on native HTML elements and interacts with web components through the vue: prefix.
What :is accepts
<script setup>
import { shallowRef } from 'vue'
import AlertBox from './AlertBox.vue'
import InfoBox from './InfoBox.vue'
const currentComponent = shallowRef(AlertBox)
</script>
<template>
<!-- 1. Component definition object (recommended) -->
<component :is="currentComponent" />
<!-- 2. Registered component name (string) -->
<component is="AlertBox" />
<!-- 3. HTML element name (string) -->
<component is="div" />
<!-- 4. Inline render function -->
<component :is="() => h('span', 'hello')" />
</template>When using a reactive variable, use shallowRef instead of ref. A ref would attempt to deeply unwrap the component object, which is unnecessary and can cause issues with complex component definitions.
is on native HTML elements
The is attribute on regular HTML elements behaves differently than on <component>. It follows the HTML spec for customized built-in elements:
<!-- HTML spec behavior: "is" on native elements creates customized built-in elements -->
<button is="my-custom-button">Click</button>
<!-- This tells the browser to upgrade the <button> with a custom element class -->To render a Vue component as a replacement for a native element, use the vue: prefix:
<!-- This renders the MyButton Vue component, not a native <button> -->
<button is="vue:MyButton">Click</button>
<!-- Useful when you need a Vue component inside elements that restrict children -->
<table>
<tr is="vue:MyTableRow"></tr>
</table>The <table> problem
HTML parsing rules restrict which elements can appear inside <table>, <ul>, <ol>, and <select>. The browser moves invalid children outside these elements before Vue even sees the DOM:
<!-- BAD: browser moves <BlogPost> outside <table> during HTML parsing -->
<table>
<BlogPost /> <!-- ends up above the table in the DOM -->
</table>
<!-- GOOD: use is="vue:" to bypass the restriction -->
<table>
<tr is="vue:BlogPost"></tr>
</table>This is only an issue when templates are parsed as HTML (in-DOM templates). SFCs compiled with Vite don't have this problem because the template is compiled at build time, not parsed as HTML.
Dynamic rendering with a component map
A common pattern for rendering different components based on data:
<script setup>
import TextBlock from './TextBlock.vue'
import ImageBlock from './ImageBlock.vue'
import VideoBlock from './VideoBlock.vue'
import type { Component } from 'vue'
const blockComponents: Record<string, Component> = {
text: TextBlock,
image: ImageBlock,
video: VideoBlock
}
const blocks = ref([
{ type: 'text', content: 'Hello' },
{ type: 'image', src: '/photo.jpg' },
{ type: 'text', content: 'World' },
{ type: 'video', src: '/clip.mp4' }
])
</script>
<template>
<component
v-for="(block, i) in blocks"
:key="i"
:is="blockComponents[block.type]"
v-bind="block"
/>
</template>This pattern is cleaner than a chain of v-if/v-else-if and scales to any number of block types without modifying the template.
Resolution order
When :is receives a string, Vue resolves it in this order:
- Locally registered components (via
importin<script setup>) - Globally registered components (
app.component('name', ...)) - Native HTML elements (
div,span,table, etc.)
If the string doesn't match any component or HTML element, Vue renders nothing and warns in development.
Combining with KeepAlive and Transition
<template>
<KeepAlive :max="5">
<Transition name="fade" mode="out-in">
<component :is="currentTab" :key="currentTabName" />
</Transition>
</KeepAlive>
</template>Add :key when using <Transition> so Vue treats each component switch as a transition between distinct elements rather than patching the same component.