Primitives
Collapsible
A region that one trigger shows and hides.
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger, Field, Input } from 'ferry-ui'
import { ChevronRight } from 'lucide-react'
export default function CollapsibleHero() {
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Field label="Project name">
<Input defaultValue="Website redesign" />
</Field>
<Collapsible className="flex flex-col gap-4">
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className="group w-fit"
icon={<ChevronRight className="transition-transform group-data-[state=open]:rotate-90" />}
>
Advanced options
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="flex flex-col gap-4">
<Field label="URL slug">
<Input mono defaultValue="website-redesign" />
</Field>
<Field label="Task prefix">
<Input mono defaultValue="WEB" />
</Field>
</CollapsibleContent>
</Collapsible>
</div>
)
}Import the parts and put them together.
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ferry-ui'
<Collapsible>
<CollapsibleTrigger />
<CollapsibleContent />
</Collapsible>| Part | Role |
|---|---|
Collapsible | Holds the open state. |
CollapsibleTrigger | The button that opens and closes the region. It sets aria-expanded. |
CollapsibleContent | The region. It unmounts when the collapsible is closed. |
Use asChild to make a Button the trigger. The trigger and the content have a data-state attribute with the value open or closed. Use it in your classes, for example to turn a chevron.
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className="group"
icon={<ChevronRight className="transition-transform group-data-[state=open]:rotate-90" />}
>
Advanced options
</Button>
</CollapsibleTrigger>A collapsible holds its open state by default. Use defaultOpen to open it at the start.
To control the state, pass open and onOpenChange. Use this when other content follows the state, for example the label of the trigger.
import * as React from 'react'
import { Button, Card, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ferry-ui'
const MEMBERS = ['Maya Chen', 'Liam Novak', 'Sara Ortiz', 'Tom Becker', 'Aiko Tanaka', 'Omar Haddad']
export default function CollapsibleControlled() {
const [open, setOpen] = React.useState(false)
const first = MEMBERS.slice(0, 3)
const rest = MEMBERS.slice(3)
return (
<Card className="w-full max-w-sm">
<Collapsible open={open} onOpenChange={setOpen}>
<ul className="divide-y text-[13px] text-foreground">
{first.map((member) => (
<li key={member} className="px-4 py-2.5">
{member}
</li>
))}
</ul>
<CollapsibleContent asChild>
<ul className="divide-y border-t text-[13px] text-foreground">
{rest.map((member) => (
<li key={member} className="px-4 py-2.5">
{member}
</li>
))}
</ul>
</CollapsibleContent>
<div className="border-t p-1.5">
<CollapsibleTrigger asChild>
<Button variant="ghost" size="tiny" className="w-full">
{open ? 'Show less' : `Show ${rest.length} more`}
</Button>
</CollapsibleTrigger>
</div>
</Collapsible>
</Card>
)
}The content has no animation by default. Add these classes to CollapsibleContent to slide it:
overflow-hidden data-[state=open]:animate-collapsible-down data-[state=closed]:animate-collapsible-upPut the padding on an element in the content. The animation changes the height of the content.
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ferry-ui'
import { ChevronRight } from 'lucide-react'
export default function CollapsibleAnimated() {
return (
<Collapsible className="flex w-full max-w-sm flex-col">
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className="group w-fit"
icon={<ChevronRight className="transition-transform group-data-[state=open]:rotate-90" />}
>
Payment terms
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
{/* The padding is on an inner element: the animation changes the height of the content. */}
<p className="px-2.5 pt-2 text-[13px] text-foreground-light">
The customer pays each invoice in 30 days. A late payment adds a fee of 2% to the next invoice.
</p>
</CollapsibleContent>
</Collapsible>
)
}Set disabled on Collapsible to keep its state. The trigger refuses clicks.
import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ferry-ui'
import { ChevronRight } from 'lucide-react'
export default function CollapsibleDisabled() {
return (
<Collapsible disabled className="flex flex-col gap-2">
<CollapsibleTrigger asChild>
<Button variant="ghost" icon={<ChevronRight />}>
Advanced options
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="text-[13px] text-foreground-light">
The options of the project.
</CollapsibleContent>
</Collapsible>
)
}Each part also accepts the props of its Radix UI primitive and the attributes of its element.
| Prop | Type | Default |
|---|---|---|
defaultOpen | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
open | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
disabled | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
onOpenChange | ((open: boolean) => void) | - |
| See the element or the Radix UI primitive this part renders. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||
| Prop | Type | Default |
|---|---|---|
forceMount | true | - |
Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. | ||
asChild | boolean | - |
| See the element or the Radix UI primitive this part renders. | ||