The compound components pattern helps you build components that work together like a team. It makes your UI easier to use and customize by grouping related parts inside one main component.
Compound components pattern in Vue
<script setup> import { provide, inject, ref } from 'vue' const TabContext = Symbol('TabContext') function useTabs() { const activeTab = ref(null) function setActiveTab(name) { activeTab.value = name } provide(TabContext, { activeTab, setActiveTab }) } function useTab() { const context = inject(TabContext) if (!context) throw new Error('useTab must be used inside Tabs') return context } </script>
Use provide and inject to share state between compound components.
Each subcomponent accesses shared state via inject to stay connected.
<template>
<Tabs>
<TabList>
<Tab name="home">Home</Tab>
<Tab name="profile">Profile</Tab>
</TabList>
<TabPanels>
<TabPanel name="home">Welcome Home!</TabPanel>
<TabPanel name="profile">User Profile Info</TabPanel>
</TabPanels>
</Tabs>
</template><script setup> import { ref, provide, inject } from 'vue' const TabsSymbol = Symbol() const Tabs = { setup(_, { slots }) { const active = ref(null) function setActive(name) { active.value = name } provide(TabsSymbol, { active, setActive }) return () => slots.default() } } const Tab = { props: ['name'], setup(props, { slots }) { const { active, setActive } = inject(TabsSymbol) const isActive = () => active.value === props.name return () => ( <button aria-selected={isActive()} onClick={() => setActive(props.name)} > {slots.default()} </button> ) } } </script>
This complete example creates a tabs interface using the compound components pattern. Clicking a tab button changes the active tab and shows the matching content panel.
<script setup> import { ref, provide, inject, defineComponent, h } from 'vue' const TabsSymbol = Symbol('Tabs') const Tabs = defineComponent({ setup(_, { slots }) { const activeTab = ref(null) function setActiveTab(name) { activeTab.value = name } provide(TabsSymbol, { activeTab, setActiveTab }) return () => h('div', { role: 'tablist' }, slots.default()) } }) const Tab = defineComponent({ props: { name: String }, setup(props, { slots }) { const context = inject(TabsSymbol) if (!context) throw new Error('Tab must be used inside Tabs') const isActive = () => context.activeTab.value === props.name function onClick() { context.setActiveTab(props.name) } return () => h('button', { role: 'tab', 'aria-selected': isActive(), onClick }, slots.default()) } }) const TabPanel = defineComponent({ props: { name: String }, setup(props, { slots }) { const context = inject(TabsSymbol) if (!context) throw new Error('TabPanel must be used inside Tabs') const isVisible = () => context.activeTab.value === props.name return () => isVisible() ? h('div', { role: 'tabpanel' }, slots.default()) : null } }) </script> <template> <Tabs> <Tab name="first">First Tab</Tab> <Tab name="second">Second Tab</Tab> <TabPanel name="first">Content for the first tab.</TabPanel> <TabPanel name="second">Content for the second tab.</TabPanel> </Tabs> </template>
Make sure all compound components are used inside the main parent component to access shared state.
Use semantic roles like tablist, tab, and tabpanel for accessibility.
Compound components keep your UI flexible and easy to maintain by separating concerns.
The compound components pattern groups related UI parts inside one main component.
Use provide and inject in Vue to share state between these parts.
This pattern helps build flexible, accessible, and easy-to-use components like tabs or dropdowns.
Practice
Solution
Step 1: Understand the pattern goal
The compound components pattern groups related UI parts to work together inside one main component.Step 2: Compare options
Only To group related UI parts inside one main component for better organization correctly describes grouping related UI parts for better organization and communication.Final Answer:
To group related UI parts inside one main component for better organization -> Option AQuick Check:
Compound components group related parts = B [OK]
- Confusing unrelated components grouping
- Ignoring state sharing between parts
- Thinking it avoids provide/inject
Solution
Step 1: Identify state sharing methods
Vue offers several ways to share data, but for compound components, provide and inject are designed for parent-child communication without prop drilling.Step 2: Match pattern usage
Compound components use provide/inject to share state internally, unlike props or emit which are for direct parent-child or event communication.Final Answer:
provide and inject -> Option AQuick Check:
State sharing in compound components = provide/inject [OK]
- Choosing props which require manual passing
- Confusing emit with state sharing
- Thinking v-model is for internal state sharing
const TabGroup = {
setup() {
provide('activeTab', ref('home'))
},
template: ``
}
const Tab = {
inject: ['activeTab'],
props: ['name'],
template: `{{ name }}`
}
// Usage:
//
//
//
//
Solution
Step 1: Understand provide/inject in Options API
TabGroup provides 'activeTab' as ref('home'). Tab injects ['activeTab'], receiving the ref object (not auto-unwrapped in Options API).Step 2: Evaluate rendering logic
In template, activeTab (ref object) === name (string) is always false. No 'active' class applied to either button.Final Answer:
<button>home</button><button>profile</button> -> Option BQuick Check:
Options API inject ref === string = false [OK]
- Assuming inject returns raw value, not ref
- Confusing which tab is active
- Thinking activeTab is not reactive
const Dropdown = {
setup() {
const open = ref(false)
provide('open', open)
},
template: ``
}
const DropdownToggle = {
inject: ['open'],
template: `Toggle`
}
Solution
Step 1: Check inject handling in Options API
Injected refs via inject: ['open'] are not auto-unwrapped in Options API templates. 'open' is the ref object.Step 2: Understand mutation and reactivity issue
@click="open = !open" reassigns a boolean to the ref variable (breaks link to original ref) and lacks .value access, making the ref unusable reactively as boolean state.Final Answer:
The 'open' ref is not reactive inside DropdownToggle -> Option DQuick Check:
Options API injected ref needs .value [OK]
- Thinking provider setup needs to return state for provide
- Template syntax looks invalid
- Provide key seems missing
Solution
Step 1: Identify compound components pattern usage
The pattern uses provide/inject to share state like activeTab across nested components without prop drilling.Step 2: Evaluate options for state management
Use provide to share activeTab ref in Tabs, inject in TabList and TabPanels, update activeTab on Tab click correctly uses provide to share activeTab ref and inject in child components, updating activeTab on tab click to reflect changes.Final Answer:
Use provide to share activeTab ref in Tabs, inject in TabList and TabPanels, update activeTab on Tab click -> Option CQuick Check:
Compound components share state via provide/inject = D [OK]
- Using props and events causing complex prop drilling
- Using global event bus which is not recommended
- Keeping state local in each tab causing no sync
Ready to prove it?
Pass the test and this pattern moves unknown
