Chapter 54: Vue component Element
The <component> element (used almost exclusively with the :is attribute → <component :is=”…”>)
This is not a normal HTML tag — it’s a special placeholder that Vue provides so you can dynamically decide at runtime which real component (or even native HTML element) should be rendered at that spot in the template.
In plain words:
Instead of hard-coding <UserProfile /> or <ProductCard /> in your template, you write <component :is=”currentComponent” /> and then change the value of currentComponent whenever you want — based on tab selection, user role, route, permission, A/B test variant, data from server, etc.
It’s the Vue way of doing “dynamic component tag” — and it’s used in almost every medium or large Vue application in 2026.
Why <component :is> exists — real-life situations you will face
- Tabbed interfaces / dashboard widgets Different tabs need to show completely different components
- Role-based or permission-based UI Admin sees AdminDashboard, Editor sees EditorPanel, Guest sees ReadOnlyView
- Lazy-loading heavy / expensive components Only load ChartLibrary when user clicks “Analytics” tab
- Polymorphic / type-based rendering In a feed: render PostCard / StoryCard / AdCard / CommentCard based on item.type
- Wizard / multi-step forms Step 1 → PersonalInfoForm, Step 2 → PaymentDetails, Step 3 → Review
- CMS / page-builder style apps JSON from backend says “render HeroBanner component here, then FeatureGrid, then CTA”
- A/B testing or feature flags Show VariantA or VariantB component for 50% of users
- Dynamic content from server / config API returns which component should be used for this block
Basic Syntax – The canonical form
|
0 1 2 3 4 5 6 |
<component :is="currentComponent" /> |
:is can receive:
- A component definition (imported component object)
- A string (component name — only if globally registered)
- A dynamic import function (() => import(‘./SomeView.vue’))
- null or undefined → nothing is rendered
Real, Complete, Production-Ready Example
Tabbed Dashboard with Lazy-Loading + KeepAlive
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
<!-- src/views/DashboardView.vue --> <template> <div class="dashboard"> <h1>My Dashboard</h1> <div class="tabs"> <button v-for="tab in tabs" :key="tab.name" :class="{ active: activeTab === tab.name }" @click="activeTab = tab.name" > {{ tab.label }} </button> </div> <!-- The magic built-in element --> <KeepAlive> <component :is="currentTabComponent" :key="activeTab" <!-- important: forces remount if needed --> /> </KeepAlive> <div v-if="!currentTabComponent" class="loading"> Loading tab content... </div> </div> </template> <script setup lang="ts"> import { ref, computed, defineAsyncComponent } from 'vue' // 1. Sync component (small & always needed) import Overview from '@/components/dashboard/Overview.vue' // 2. Async / lazy-loaded components (big bundles — load only when needed) const Analytics = defineAsyncComponent(() => import('@/components/dashboard/Analytics.vue')) const Users = defineAsyncComponent(() => import('@/components/dashboard/Users.vue')) const Settings = defineAsyncComponent(() => import('@/components/dashboard/Settings.vue')) // 3. Tab configuration – very clean & scalable pattern const tabs = [ { name: 'overview', label: 'Overview', component: Overview }, { name: 'analytics', label: 'Analytics', component: Analytics }, { name: 'users', label: 'Users', component: Users }, { name: 'settings', label: 'Settings', component: Settings } ] // 4. Reactive state const activeTab = ref('overview') // 5. Computed – resolves which component to render right now const currentTabComponent = computed(() => { const tab = tabs.find(t => t.name === activeTab.value) return tab?.component || null }) </script> <style scoped> .dashboard { padding: 2rem; max-width: 1400px; margin: 0 auto; } .tabs { display: flex; gap: 0.5rem; margin-bottom: 2rem; border-bottom: 1px solid #e5e7eb; } .tabs button { padding: 0.8rem 1.5rem; background: none; border: none; border-bottom: 3px solid transparent; font-weight: 500; cursor: pointer; transition: all 0.2s; } .tabs .active { border-bottom-color: #3b82f6; color: #3b82f6; } .loading { text-align: center; padding: 4rem; color: #6b7280; font-size: 1.2rem; } </style> |
Different Valid Ways to Use :is (All Common in 2026)
| Style | Code Example | When to use it |
|---|---|---|
| Direct imported component | import Profile from ‘./Profile.vue’ <component :is=”Profile” /> | Small, always-used components |
| Async / lazy-loaded | defineAsyncComponent(() => import(‘./HeavyChart.vue’)) | Large bundles, route-based lazy loading |
| Object map / dictionary | const views = { home: HomeView, profile: ProfileView } <component :is=”views[activeView]” /> | Tabs, wizards, role-based views |
| String name (global registration) | app.component(‘UserProfile’, UserProfile) <component is=”UserProfile” /> | Rare in modern apps (local imports preferred) |
| Dynamic import from string (CMS) | <component :is=”() => import(@/components/${block.type}.vue)” /> | Headless CMS, page builders |
Important Gotchas & Pro Tips (2026 Edition)
-
Always add :key when :is changes frequently
vue0123456<component :is="currentTab" :key="activeTab" />→ Prevents state leakage (form inputs, scroll position) when switching
-
Use <KeepAlive> to preserve state across switches
vue012345678<KeepAlive><component :is="currentTab" /></KeepAlive>→ Tabs remember form data, scroll position, internal state — huge UX win
-
Loading & error handling for async components
vue0123456789101112const MyComp = defineAsyncComponent({loader: () => import('./MyComp.vue'),loadingComponent: LoadingSpinner,errorComponent: ErrorFallback,delay: 200, // show loader only after 200mstimeout: 5000 // fail after 5s}) -
SSR / hydration — sync components work fine, async ones are client-only
-
TypeScript — works beautifully
TypeScript0123456const current = computed(() => views[activeTab.value] as Component)
Summary Table – When You Reach for <component :is>
| Use Case | Recommended :is Pattern | Bonus Tip / Pattern |
|---|---|---|
| Tab / wizard navigation | Map object + computed | + <KeepAlive> + :key |
| Role/permission-based UI | views[role] or ternary | Combine with v-if for fallback |
| Lazy heavy components | defineAsyncComponent(() => import(…)) | + loading/error states |
| CMS / config-driven rendering | Dynamic import from string | Validate allowed component names |
| Polymorphic list items | <component :is=”getComponentType(item)” /> | Often inside v-for |
Your Mini Practice After This Lesson
- Create 3 small tab components: Overview, Stats, Profile
- Build a tabbed interface using <component :is> + object map
- Add <KeepAlive> → switch tabs → see that form inputs / scroll position survive
- Add loading spinner for async tabs
Any part confusing? Want to see:
- Dynamic components + <Transition> for smooth tab switch animations?
- Real CMS-style rendering from JSON config?
- Error boundary around async dynamic components?
- Dynamic components inside v-for (polymorphic list items)?
Just tell me — we’ll build the next powerful dynamic example together 🚀
