Collapsible
Collapsible is a convenience wrapper for the Radix Collapsible primitive. It keeps trigger and content composition concise while still supporting controlled or uncontrolled state.
Installation
1. Install dependencies:
npm install @radix-ui/react-collapsible2. Install the Collapsible primitive via shadcn:
npx shadcn@latest add collapsible3. Copy the wrapper source:
- Source in this monorepo:
packages/ui/src/common/overlays/collapsible-wrapper.tsx - Place in your project:
components/common/overlays/collapsible-wrapper.tsx - Required utility:
lib/utils.ts(forcnhelper) - Required primitive:
components/ui/collapsible.tsx
4. Update import paths to match your project structure.
Usage
import { CollapsibleWrapper as Collapsible } from "@hoag/ui/collapsible-wrapper";
import { Button } from "@hoag/ui/components/button";
export default function Demo() {
return (
<Collapsible trigger={<Button variant="outline">Toggle Details</Button>}>
<div>Hidden content goes here.</div>
</Collapsible>
);
}Default
Added improved wrappers for overlays, docs navigation updates, and UX polishing.
<Collapsible
trigger={<Button variant="outline">Toggle release notes</Button>}
defaultOpen
contentContainerClassName="pt-2"
>
<div className="rounded-md border p-3 text-sm text-muted-foreground">
Added improved wrappers for overlays, docs navigation updates, and UX
polishing.
</div>
</Collapsible>Trigger Modes
triggerAsChild (default)
Use this mode when your trigger is already a button-like component.
<Collapsible trigger={<Button variant="secondary">Show changelog</Button>}>
<div>Version 2.1 introduced keyboard navigation improvements.</div>
</Collapsible>Native trigger button (triggerAsChild={false})
Use this mode when you want the wrapper to render its own trigger button.
<Collapsible
trigger="Toggle details"
triggerAsChild={false}
triggerClassName="rounded-md border px-3 py-2 text-sm"
>
<div>The trigger is rendered by CollapsibleTrigger.</div>
</Collapsible>Disabled
<Collapsible trigger={<Button variant="outline">Cannot open</Button>} disabled>
<div>This content cannot be expanded.</div>
</Collapsible>Controlled Mode
Use controlled mode when open state depends on parent state or app logic.
import { useState } from "react";
export default function ControlledExample() {
const [open, setOpen] = useState(false);
return (
<Collapsible
open={open}
onOpenChange={setOpen}
trigger={
<Button variant="secondary">{open ? "Hide" : "Show"} details</Button>
}
>
<div className="rounded-md border p-3">
This content is controlled by parent state.
</div>
</Collapsible>
);
}Tips
- Keep
triggerAsChildenabled when usingButton,Link, or other custom trigger components. - Use
contentContainerClassNamefor animating/spacing the collapsible region. - Use
contentClassNamefor styling the inner content wrapper. - Use controlled mode (
open+onOpenChange) when syncing UI state with URL/query/form logic.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | Class for root Collapsible |
trigger | ReactNode | — | Trigger element |
triggerAsChild | boolean | true | Render trigger as child |
triggerClassName | string | — | Trigger class override |
contentContainerClassName | string | — | Class for CollapsibleContent |
contentClassName | string | — | Class for inner content wrapper |
open | boolean | — | Controlled open state |
onOpenChange | (open: boolean) => void | — | Open state callback |
defaultOpen | boolean | — | Initial open state |
disabled | boolean | false | Disable interactions |
children | ReactNode | — | Content rendered inside collapsible area |