Skip to main content

nachui-compound-components

figueroaignacio/ui-skills · View source

$npx skills add github:figueroaignacio/ui-skills/skills/nachui-compound-components

Triggers when: “Use when building, extending, or consuming compound components — components made of multiple sub-parts assembled via dot notation (e.g. Dialog.Trigger, Card.Header, Accordion.Item).”

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 export
2const 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});
11
12// Usage — all parts live under one import
13import { Dialog } from '@repo/ui/components/dialog';
14
15<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

ComponentSub-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 context
2const AccordionContext = React.createContext<AccordionContextValue | null>(null);
3
4// 2. Guard hook — throws if used outside provider
5const 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};
10
11// 3. Root provides context
12const AccordionRoot = ({ children, type = 'single', ...props }) => {
13 const [openItems, setOpenItems] = React.useState<string[]>([]);
14 // ...state logic
15
16 return (
17 <AccordionContext value={{ type, openItems, toggleItem }}>
18 <div {...props}>{children}</div>
19 </AccordionContext>
20 );
21};
22
23// 4. Sub-part consumes context
24const AccordionTrigger = ({ value, children }) => {
25 const { openItems, toggleItem } = useAccordionContext();
26 const isOpen = openItems.includes(value);
27 // ...
28};
29
30// 5. Assemble and export
31const Accordion = Object.assign(AccordionRoot, {
32 Item: AccordionItem,
33 Trigger: AccordionTrigger,
34 Content: AccordionContent,
35});
36
37export { Accordion };

Controlled vs Uncontrolled

All stateful compound components support both modes.
1// Uncontrolled — internal state, no props needed
2<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>
8
9// Controlled — you own the state
10const [open, setOpen] = React.useState<string[]>([])
11
12<Accordion value={open} onValueChange={setOpen}>
13 ...
14</Accordion>

Usage Examples

Card

1import { Card } from '@repo/ui/components/card';
2
3<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>
15
16// Variants: default | outline | ghost
17// compact prop: reduces padding
18<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';
2
3// 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>
14
15// Multiple open simultaneously
16<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

  1. Never skip the root — sub-parts thrown without their root will throw a runtime error from the context guard.
  2. Always set displayName on every sub-component for React DevTools legibility.
  3. Hoist animation constants to module level — never define variants objects inside component functions.
  4. Use React.use(Context) (React 19 API), not React.useContext(Context) in new components.
  5. Preserve asChild pattern on Trigger and Close sub-parts so consumers can swap the rendered element.