Skip to main content

Compound Components

Learn the compound component pattern for building flexible, typed UI APIs with shared state and explicit slots.

Compound components let you expose a single root component together with named sub-components. This pattern is ideal when a UI primitive has multiple interactive parts that must stay coordinated, such as tabs, menus, or form fields.
In NachUI, we create them by defining a root component and then attaching named child units with Object.assign:
1const Tabs = Object.assign(TabsRoot, {
2 List: TabsList,
3 Trigger: TabsTrigger,
4 Content: TabsContent,
5});
6
7export { Tabs };
This is the pattern used across the package to keep the public API simple and consistent, with <Tabs.List />, <Tabs.Trigger />, and <Tabs.Content />.

Why use compound components

Compound components make your API:
  • explicit: each slot is a named sub-component instead of a generic children blob
  • composable: users can mix and match child pieces inside the root
  • typed: TypeScript can infer props for each sub-component independently
  • stateful: the root can share state through context instead of prop drilling

How it works

The root component owns the logic and provides context. Sub-components read from that context to render the correct behavior.
1function Tabs({ children, defaultValue }: TabsProps) {
2 const [value, setValue] = useState(defaultValue);
3
4 return (
5 <TabsContext.Provider value={{ value, setValue }}>
6 <div className="space-y-4">{children}</div>
7 </TabsContext.Provider>
8 );
9}
10
11Tabs.List = function TabsList({ children }: { children: ReactNode }) {
12 return <div className="flex gap-2">{children}</div>;
13};
14
15Tabs.Trigger = function TabsTrigger({ value, children }: TabsTriggerProps) {
16 const context = useTabsContext();
17 const selected = context.value === value;
18
19 return (
20 <button
21 type="button"
22 className={cn('rounded-md px-3 py-2', selected ? 'bg-primary text-white' : 'bg-muted')}
23 onClick={() => context.setValue(value)}
24 >
25 {children}
26 </button>
27 );
28};
29
30Tabs.Content = function TabsContent({ value, children }: TabsContentProps) {
31 const context = useTabsContext();
32 return context.value === value ? <div>{children}</div> : null;
33};

Example usage

1<Tabs defaultValue="overview">
2 <Tabs.List>
3 <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
4 <Tabs.Trigger value="details">Details</Tabs.Trigger>
5 </Tabs.List>
6
7 <Tabs.Content value="overview">Overview content</Tabs.Content>
8 <Tabs.Content value="details">Details content</Tabs.Content>
9</Tabs>

When to use this pattern

Use compound components when a UI primitive has multiple named pieces that depend on shared state. Good candidates are:
  • tabs
  • accordions
  • dropdown menus
  • dialog headers / footers
  • form controls with label and description slots
If the component is simple and only needs one element, prefer a single component API instead.

Best practices

  • export sub-components as static members on the root export
  • keep the root responsible for shared state and class generation
  • avoid deeply nested contexts unless the hierarchy is required
  • document the sub-component names and expected slot order
  • use clear TypeScript props for each child component

Why this architecture fits NachUI

NachUI favors predictable, composable primitives. Compound components keep UI APIs readable and make it easy to add optional slots or variants without breaking consumptions.
Found something to improve?

Notice a bug, typo, or missing detail on this page? Help us make the documentation better by opening a GitHub issue.

Create an Issue