NachUI Compound Components
You are working with NachUI's compound component pattern. Every interactive component in
@repo/ui uses this pattern. Learn it once, apply it everywhere.What Is a Compound Component?
A compound component is a single exported name that bundles multiple sub-components via
Object.assign. Each sub-part is accessed via dot notation.1// The export2const Dialog = Object.assign(DialogRoot, {3 Trigger: DialogTrigger,4 Content: DialogContent,5 Header: DialogHeader,6 Title: DialogTitle,7 Description: DialogDescription,8 Footer: DialogFooter,9 Close: DialogClose,10});1112// Usage — all parts live under one import13import { Dialog } from '@repo/ui/components/dialog';1415<Dialog>16 <Dialog.Trigger>Open</Dialog.Trigger>17 <Dialog.Content>18 <Dialog.Header>19 <Dialog.Title>Confirm</Dialog.Title>20 <Dialog.Description>Are you sure?</Dialog.Description>21 </Dialog.Header>22 <Dialog.Footer>23 <Dialog.Close>Cancel</Dialog.Close>24 <Button>Confirm</Button>25 </Dialog.Footer>26 </Dialog.Content>27</Dialog>;
Compound Component Map
| Component | Sub-parts |
|---|---|
Accordion | .Item .Trigger .Content |
Banner | .Icon .Title .Description .Action |
Breadcrumb | .Item .Link .Separator .Ellipsis |
Button | .Group |
Card | .Header .Title .Description .Content .Footer |
Collapsible | .Trigger .Content |
Dialog | .Trigger .Content .Header .Title .Description .Footer .Close .Overlay .Portal |
Drawer | .Trigger .Content .Header .Title .Description .Footer .Close .Overlay .Portal |
DropdownMenu | .Trigger .Content .Item .Separator .Label .Group |
Files | .Tree .Item .Folder |
Popover | .Trigger .Content .Arrow |
Steps | .Item .Indicator .Content |
Tabs | .List .Trigger .Content |
Toast | .Title .Description .Action .Close |
Tooltip | .Trigger .Content .Arrow |
Pattern: Context + Sub-components
Internally, compound components share state via React Context. The root component provides the context; sub-parts consume it.
1// 1. Define context2const AccordionContext = React.createContext<AccordionContextValue | null>(null);34// 2. Guard hook — throws if used outside provider5const useAccordionContext = () => {6 const context = React.use(AccordionContext);7 if (!context) throw new Error('Accordion components must be used within an Accordion');8 return context;9};1011// 3. Root provides context12const AccordionRoot = ({ children, type = 'single', ...props }) => {13 const [openItems, setOpenItems] = React.useState<string[]>([]);14 // ...state logic1516 return (17 <AccordionContext value={{ type, openItems, toggleItem }}>18 <div {...props}>{children}</div>19 </AccordionContext>20 );21};2223// 4. Sub-part consumes context24const AccordionTrigger = ({ value, children }) => {25 const { openItems, toggleItem } = useAccordionContext();26 const isOpen = openItems.includes(value);27 // ...28};2930// 5. Assemble and export31const Accordion = Object.assign(AccordionRoot, {32 Item: AccordionItem,33 Trigger: AccordionTrigger,34 Content: AccordionContent,35});3637export { Accordion };
Controlled vs Uncontrolled
All stateful compound components support both modes.
1// Uncontrolled — internal state, no props needed2<Accordion defaultValue="item-1">3 <Accordion.Item value="item-1">4 <Accordion.Trigger value="item-1">Question</Accordion.Trigger>5 <Accordion.Content value="item-1">Answer</Accordion.Content>6 </Accordion.Item>7</Accordion>89// Controlled — you own the state10const [open, setOpen] = React.useState<string[]>([])1112<Accordion value={open} onValueChange={setOpen}>13 ...14</Accordion>
Usage Examples
Card
1import { Card } from '@repo/ui/components/card';23<Card>4 <Card.Header>5 <Card.Title>Dashboard</Card.Title>6 <Card.Description>Overview of your metrics</Card.Description>7 </Card.Header>8 <Card.Content>9 {/* main content */}10 </Card.Content>11 <Card.Footer align="end">12 <Button variant="outline">Export</Button>13 </Card.Footer>14</Card>1516// Variants: default | outline | ghost17// compact prop: reduces padding18<Card variant="outline">19 <Card.Header compact>20 <Card.Title>Compact Card</Card.Title>21 </Card.Header>22 <Card.Content compact>...</Card.Content>23</Card>
Accordion
1import { Accordion } from '@repo/ui/components/accordion';23// Single open at a time (default)4<Accordion type="single" defaultValue="q1">5 <Accordion.Item value="q1">6 <Accordion.Trigger value="q1">What is NachUI?</Accordion.Trigger>7 <Accordion.Content value="q1">A component library for React.</Accordion.Content>8 </Accordion.Item>9 <Accordion.Item value="q2">10 <Accordion.Trigger value="q2">Is it open source?</Accordion.Trigger>11 <Accordion.Content value="q2">Yes.</Accordion.Content>12 </Accordion.Item>13</Accordion>1415// Multiple open simultaneously16<Accordion type="multiple">17 ...18</Accordion>
Tabs
1import { Tabs } from '@repo/ui/components/tabs';2<Tabs defaultValue="overview">3 <Tabs.List>4 <Tabs.Trigger value="overview">Overview</Tabs.Trigger>5 <Tabs.Trigger value="settings">Settings</Tabs.Trigger>6 </Tabs.List>7 <Tabs.Content value="overview">8 <p>Overview content</p>9 </Tabs.Content>10 <Tabs.Content value="settings">11 <p>Settings content</p>12 </Tabs.Content>13</Tabs>;
Dialog
1import { Dialog } from '@repo/ui/components/dialog';2import { Button } from '@repo/ui/components/button';3<Dialog>4 <Dialog.Trigger asChild>5 <Button variant="outline">Open Dialog</Button>6 </Dialog.Trigger>7 <Dialog.Content>8 <Dialog.Header>9 <Dialog.Title>Delete item</Dialog.Title>10 <Dialog.Description>This action cannot be undone.</Dialog.Description>11 </Dialog.Header>12 <Dialog.Footer>13 <Dialog.Close asChild>14 <Button variant="outline">Cancel</Button>15 </Dialog.Close>16 <Button variant="destructive">Delete</Button>17 </Dialog.Footer>18 </Dialog.Content>19</Dialog>;
Tooltip
1import { Tooltip } from '@repo/ui/components/tooltip';2<Tooltip>3 <Tooltip.Trigger asChild>4 <Button variant="outline" size="icon">5 ?6 </Button>7 </Tooltip.Trigger>8 <Tooltip.Content>More information here.</Tooltip.Content>9</Tooltip>;
Rules When Extending
- Never skip the root — sub-parts thrown without their root will throw a runtime error from the context guard.
- Always set
displayNameon every sub-component for React DevTools legibility. - Hoist animation constants to module level — never define
variantsobjects inside component functions. - Use
React.use(Context)(React 19 API), notReact.useContext(Context)in new components. - Preserve
asChildpattern onTriggerandClosesub-parts so consumers can swap the rendered element.