0
0
Vueframework~5 mins

Compound components pattern in Vue

Choose your learning style9 modes available
Introduction

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.

When you want to create a custom dropdown menu with separate parts like toggle button and menu list.
When building a tabs interface where each tab and its content are separate but connected.
When making a form with grouped inputs that share state and behavior.
When you want to keep your UI code clean by splitting complex components into smaller pieces that work together.
Syntax
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.

Examples
This example shows how to use compound components to build a tabs interface with separate parts working together.
Vue
<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>
This shows how the main Tabs component shares state and the Tab component uses it to highlight the active tab.
Vue
<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>
Sample Program

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.

Vue
<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>
OutputSuccess
Important Notes

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.

Summary

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.