Chapter 45: Vue ‘is’ Attribute
The is attribute (usually written as :is because it’s almost always dynamic)
This is not a normal HTML attribute — it’s a Vue-specific magic attribute that tells Vue:
“Please render this tag name / component right here — and the name can be decided at runtime.”
In other words: :is lets you dynamically choose which component (or even native HTML element) to render at that spot in the template.
Without :is, you would have to hard-code the component tag name — like <UserProfile /> or <ProductCard />. With :is, the tag name becomes a variable — so you can switch components on the fly based on data, user role, tab selection, route, permission, A/B test variant, etc.
Real Names This Feature Has Had Over Time
- Officially called: “Dynamic Component” or “is attribute”
- Most common shorthand in templates: :is
- In code people say: “use the is prop on <component>” or “dynamic :is”
The canonical way to use it is:
|
0 1 2 3 4 5 6 |
<component :is="currentComponent" /> |
<component> is a built-in Vue placeholder tag — it doesn’t render anything itself; it just becomes whatever :is points to.
Why :is Exists – Real-Life Situations (2026)
- Tabbed interfaces / dashboard widgets Different tabs render completely different components
- Role-based / permission-based UI Admin sees AdminPanel, Editor sees EditorPanel, Guest sees ReadOnlyView
- Lazy-loading heavy components Only load ChartLibrary when user clicks “Analytics” tab
- Polymorphic / type-based rendering In a feed: render PostCard / StoryCard / AdCard based on item.type
- CMS / page-builder style apps JSON from backend says “render HeroBanner component here”
- A/B testing or feature flags Show VariantA or VariantB component
- Wizard / multi-step forms Step 1 → PersonalInfo, Step 2 → PaymentDetails, etc.
Real, Complete Example – Tabbed Dashboard with :is
|
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 88 89 90 91 92 93 94 95 96 97 98 99 |
<!-- src/views/DashboardView.vue --> <template> <div class="dashboard"> <h1>My Dashboard</h1> <!-- Tab navigation --> <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 line – dynamic component --> <component :is="currentTabComponent" :data="tabData" @some-event="handleTabEvent" keep-alive /> <!-- Optional loading / error placeholder --> <div v-if="!currentTabComponent" class="loading"> Loading tab content... </div> </div> </template> <script setup lang="ts"> import { ref, computed, defineAsyncComponent } from 'vue' // Option 1: Sync components import Overview from '@/components/dashboard/Overview.vue' // Option 2: Async / lazy-loaded (recommended for large apps) const Analytics = defineAsyncComponent(() => import('@/components/dashboard/Analytics.vue')) const Users = defineAsyncComponent(() => import('@/components/dashboard/Users.vue')) const Settings = defineAsyncComponent(() => import('@/components/dashboard/Settings.vue')) // 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 } ] // Reactive state const activeTab = ref('overview') // Computed – resolves which component to render const currentTabComponent = computed(() => { const tab = tabs.find(t => t.name === activeTab.value) return tab?.component || null }) // Optional: pass data down to whatever component is active const tabData = computed(() => ({ activeTab: activeTab.value, timestamp: new Date().toISOString() })) function handleTabEvent(payload: any) { console.log('Event from current tab:', payload) } </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 Ways to Use :is (All Valid & Useful)
-
Direct imported component
vue01234567import Profile from '@/views/ProfileView.vue'<component :is="Profile" /> -
Async / lazy component (best for performance)
vue01234567const HeavyChart = defineAsyncComponent(() => import('@/components/HeavyChart.vue'))<component :is="showAnalytics ? HeavyChart : null" /> -
String name (needs global registration – less common now)
vue01234567app.component('UserProfile', UserProfile)<component is="UserProfile" /> -
Object map / dictionary (very clean for tabs, wizards)
JavaScript012345678910111213const views = {home: HomeView,profile: ProfileView,settings: SettingsView,admin: AdminDashboard}<component :is="views[activeView]" /> -
Dynamic import from string (CMS / config driven – advanced)
JavaScript01234567const componentName = 'HeroBanner' // from API / config<component :is="() => import(`@/components/blocks/{componentName}.vue`)" />
Important Gotchas & Pro Tips (2026 Edition)
-
Always add :key when :is changes frequently
vue0123456<component :is="currentTab" :key="activeTab" />→ Prevents state leakage between tabs (form inputs, scroll position)
-
Use <KeepAlive> to preserve state when switching
vue012345678<KeepAlive><component :is="currentTab" /></KeepAlive>→ Tabs remember form data, scroll position, internal state
-
Loading / error states 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 — dynamic components work fine, but async ones are client-only by nature
-
TypeScript — works perfectly with generics
TypeScript0123456const current = computed(() => views[activeTab.value] as Component)
Summary Table – Dynamic Components with :is
| Use Case | Recommended :is Pattern | Bonus Tip / Pattern |
|---|---|---|
| Tab / wizard navigation | Map object + computed | + <KeepAlive> |
| 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 :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 🚀
Happy dynamic-component-ing from Hyderabad! 💙
