Disclosure
Collapsible
An expandable/collapsible content area with controlled and uncontrolled modes and built-in transition animation.
Installation
bash
pnpm dlx shadcn@latest add collapsible -c packages/reactBasic
@peduarte starred 3 repositories
@radix-ui/primitives
tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@polyui/react/collapsible"
import { Button } from "@polyui/react/button"
import { ChevronsUpDownIcon } from "lucide-react"
export function CollapsibleBasic() {
return (
<Collapsible className="flex w-full max-w-[350px] flex-col gap-2">
<div className="flex items-center justify-between gap-4 px-4">
<div className="text-sm font-semibold">@peduarte starred 3 repositories</div>
<CollapsibleTrigger render={<Button variant="ghost" size="icon-sm" />}>
<ChevronsUpDownIcon className="size-4" />
<span className="sr-only">Toggle</span>
</CollapsibleTrigger>
</div>
<div className="rounded-md border px-4 py-2 font-mono text-sm">@radix-ui/primitives</div>
<CollapsibleContent className="flex flex-col gap-2">
<div className="rounded-md border px-4 py-2 font-mono text-sm">@radix-ui/colors</div>
<div className="rounded-md border px-4 py-2 font-mono text-sm">@stitches/react</div>
</CollapsibleContent>
</Collapsible>
)
}File Tree
components.json
tsx
export function CollapsibleFileTree() {
return (
<div className="flex w-full max-w-48 flex-col gap-2">
{fileTree.map((item) => (
<FileTreeNode key={item.name} item={item} level={0} />
))}
</div>
)
}Show More
Today's task completion
- HL90%Howard Lloyd
Product Manager
- OS60%Olivia Sparks
Software Engineer
vue
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@polyui/vue/collapsible"
import { Button } from "@polyui/vue/button"
export function CollapsibleList() {
const tasks = [
{ fallback: "HL", name: "Howard Lloyd", designation: "Product Manager", percentage: 90 },
{ fallback: "OS", name: "Olivia Sparks", designation: "Software Engineer", percentage: 60 },
{ fallback: "HR", name: "Hallie Richards", designation: "UI/UX Designer", percentage: 80 },
{ fallback: "JW", name: "Jenny Wilson", designation: "Junior Developer", percentage: 15 },
]
return (
<Collapsible class="flex w-full max-w-[350px] flex-col items-start gap-4">
<div class="font-medium">Today's task completion</div>
<ul class="flex w-full flex-col gap-2">
{tasks.slice(0, 2).map((task) => (
<li key={task.name} class="flex items-start gap-4">
<div class="flex size-10 items-center justify-center rounded-full bg-muted text-sm font-medium">
{task.fallback}
</div>
<div class="flex flex-1 flex-col">
<div class="text-sm font-medium">{task.name}</div>
<p class="text-muted-foreground text-xs">{task.designation}</p>
</div>
<span class="text-muted-foreground text-sm">{task.percentage}%</span>
</li>
))}
<CollapsibleContent class="flex flex-col gap-2">
{tasks.slice(2).map((task) => (
<li key={task.name} class="flex items-start gap-4">
<div class="flex size-10 items-center justify-center rounded-full bg-muted text-sm font-medium">
{task.fallback}
</div>
<div class="flex flex-1 flex-col">
<div class="text-sm font-medium">{task.name}</div>
<p class="text-muted-foreground text-xs">{task.designation}</p>
</div>
<span class="text-muted-foreground text-sm">{task.percentage}%</span>
</li>
))}
</CollapsibleContent>
</ul>
<CollapsibleTrigger as-child>
<Button variant="outline" size="sm">
Show more / less
</Button>
</CollapsibleTrigger>
</Collapsible>
)
}Profiles
tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@polyui/react/collapsible"
import { Button } from "@polyui/react/button"
import { ChevronRightIcon, PlusIcon, UserIcon, PanelsTopLeftIcon } from "lucide-react"
import { Avatar, AvatarFallback, AvatarImage } from "@polyui/react/avatar"
export function CollapsibleProfiles() {
return (
<ul className="flex w-full max-w-[350px] flex-col gap-4">
{profileUsers.map((user) => (
<Collapsible key={user.name}>
<li className="flex flex-col gap-2">
<CollapsibleTrigger className="flex w-full items-center justify-between gap-4">
<div className="flex items-center gap-2">
<Avatar>
<AvatarImage src={user.image} alt={user.name} />
<AvatarFallback>{user.fallback}</AvatarFallback>
</Avatar>
<span className="font-medium">{user.name}</span>
</div>
<ChevronRightIcon className="size-4 transition-transform [[data-state=open]_&]:rotate-90" />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-2 pl-12">
<p className="text-muted-foreground text-sm">{user.bio}</p>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-4">
<span className="flex items-center gap-1.5 text-sm">
<UserIcon className="size-4" />
{user.followers}
</span>
<span className="flex items-center gap-1.5 text-sm">
<PanelsTopLeftIcon className="size-4" />
{user.projects}
</span>
</div>
{user.followed ? (
<Button variant="outline" className="h-7 rounded-full px-3 py-1 text-xs">
Following
</Button>
) : (
<Button className="h-7 rounded-full px-3 py-1 text-xs">
Follow
<PlusIcon />
</Button>
)}
</div>
</div>
</CollapsibleContent>
</li>
</Collapsible>
))}
</ul>
)
}Filter
Price Range
Brand
tsx
import { useState } from "react"
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@polyui/react/collapsible"
import { Checkbox } from "@polyui/react/checkbox"
import { Label } from "@polyui/react/label"
import { ChevronDownIcon } from "lucide-react"
export function CollapsibleFilter() {
const priceRanges = ["Under $25", "$25 - $50", "$50 - $100", "Over $100"]
const brands = ["Apple", "Samsung", "Sony", "LG"]
const [selectedPrices, setSelectedPrices] = useState<string[]>([])
const [selectedBrands, setSelectedBrands] = useState<string[]>(["Apple"])
return (
<div className="w-full max-w-[350px] space-y-3">
<Collapsible className="flex flex-col gap-2" defaultOpen>
<div className="flex items-center justify-between gap-4 px-4">
<div className="text-sm font-semibold">Price Range</div>
<CollapsibleTrigger className="group">
<ChevronDownIcon className="text-muted-foreground size-4 transition-transform group-data-[state=open]:rotate-180" />
</CollapsibleTrigger>
</div>
<CollapsibleContent className="flex flex-col gap-2 px-4">
{priceRanges.map((range) => (
<div key={range} className="flex items-center gap-2">
<Checkbox
id={`price-${range}`}
checked={selectedPrices.includes(range)}
onCheckedChange={(c) =>
setSelectedPrices(c ? [...selectedPrices, range] : selectedPrices.filter((r) => r !== range))
}
/>
<Label htmlFor={`price-${range}`}>{range}</Label>
</div>
))}
</CollapsibleContent>
</Collapsible>
<div className="border-t" />
<Collapsible className="flex flex-col gap-2" defaultOpen>
<div className="flex items-center justify-between gap-4 px-4">
<div className="text-sm font-semibold">Brand</div>
<CollapsibleTrigger className="group">
<ChevronDownIcon className="text-muted-foreground size-4 transition-transform group-data-[state=open]:rotate-180" />
</CollapsibleTrigger>
</div>
<CollapsibleContent className="flex flex-col gap-2 px-4">
{brands.map((brand) => (
<div key={brand} className="flex items-center gap-2">
<Checkbox
id={`brand-${brand}`}
checked={selectedBrands.includes(brand)}
onCheckedChange={(c) =>
setSelectedBrands(c ? [...selectedBrands, brand] : selectedBrands.filter((b) => b !== brand))
}
/>
<Label htmlFor={`brand-${brand}`}>{brand}</Label>
</div>
))}
</CollapsibleContent>
</Collapsible>
</div>
)
}FAQ
How can I track my order?
To track your order, simply log in to your account and navigate to the order history section. You'll find detailed information about your order status and tracking number there.
Can I cancel my order?
vue
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@polyui/vue/collapsible"
export function CollapsibleFAQ() {
return (
<div class="w-full max-w-sm space-y-4">
<div class="space-y-2">
<p class="font-medium">How can I track my order?</p>
<Collapsible defaultOpen class="space-y-2">
<CollapsibleContent>
<p class="text-sm">
To track your order, simply log in to your account and navigate to the order history section. You'll find
detailed information about your order status and tracking number there.
</p>
</CollapsibleContent>
<CollapsibleTrigger>
<span class="text-muted-foreground text-sm underline">Toggle answer</span>
</CollapsibleTrigger>
</Collapsible>
</div>
<div class="space-y-2">
<p class="font-medium">Can I cancel my order?</p>
<Collapsible class="space-y-2">
<CollapsibleContent>
<p class="text-sm">
Scheduled delivery orders can be cancelled 72 hours prior to your selected delivery date for a full
refund.
</p>
</CollapsibleContent>
<CollapsibleTrigger>
<span class="text-muted-foreground text-sm underline">Toggle answer</span>
</CollapsibleTrigger>
</Collapsible>
</div>
</div>
)
}Card
tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@polyui/react/collapsible"
import { Button } from "@polyui/react/button"
import { Card, CardTitle, CardContent, CardAction } from "@polyui/react/card"
import { ChevronUpIcon } from "lucide-react"
export function CollapsibleCard() {
return (
<Card className="w-full max-w-md pb-0">
<Collapsible>
<div className="flex items-center justify-between px-6 pb-6">
<CardTitle>How do I track my order?</CardTitle>
<CardAction>
<CollapsibleTrigger render={<Button variant="outline" size="sm" />}>
<span className="[[data-state=open]>&]:hidden">Show</span>
<span className="[[data-state=closed]>&]:hidden">Hide</span>
<ChevronUpIcon className="[[data-state=closed]>&]:rotate-180" />
</CollapsibleTrigger>
</CardAction>
</div>
<CollapsibleContent>
<CardContent className="space-y-2 px-0">
<p className="px-6">You'll receive tracking information via email once your order ships.</p>
<img
src="https://cdn.shadcnstudio.com/ss-assets/components/accordion/image-1.jpg?width=446&format=auto"
alt="Order tracking"
className="aspect-video h-64 w-full rounded-b-xl object-cover"
/>
</CardContent>
</CollapsibleContent>
</Collapsible>
</Card>
)
}Nested Menu
tsx
import { useState } from "react"
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@polyui/react/collapsible"
import { Button } from "@polyui/react/button"
import { ChevronRightIcon, ChevronDownIcon, UserIcon, CircleSmallIcon, SettingsIcon, LogOutIcon, UsersIcon } from "lucide-react"
export function CollapsibleNestedMenu() {
const [menuOpen, setMenuOpen] = useState(false)
return (
<div className="relative w-56">
<Button variant="outline" onClick={() => setMenuOpen((o) => !o)} className="w-full justify-between">
Menu with collapsible
<ChevronDownIcon className={`size-4 transition-transform ${menuOpen ? "rotate-180" : ""}`} />
</Button>
{menuOpen && (
<div className="absolute top-full mt-1 w-full rounded-md border bg-popover p-1 shadow-md z-50">
<button className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent">
<UserIcon className="size-4" />
Profile
</button>
<Collapsible>
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-accent">
<span className="flex items-center gap-2">
<SettingsIcon className="size-4" />
Settings
</span>
<ChevronRightIcon className="size-4 transition-transform [[data-state='open']>&]:rotate-90" />
</CollapsibleTrigger>
<CollapsibleContent className="pl-4">
{["Account", "Security", "Billing & plans"].map((item) => (
<button
key={item}
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
>
<CircleSmallIcon className="size-4" />
{item}
</button>
))}
</CollapsibleContent>
</Collapsible>
<Collapsible>
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-accent">
<span className="flex items-center gap-2">
<UsersIcon className="size-4" />
Team
</span>
<ChevronRightIcon className="size-4 transition-transform [[data-state='open']>&]:rotate-90" />
</CollapsibleTrigger>
<CollapsibleContent className="pl-4">
{["Members", "Invitations", "Roles"].map((item) => (
<button
key={item}
className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
>
<CircleSmallIcon className="size-4" />
{item}
</button>
))}
</CollapsibleContent>
</Collapsible>
<button className="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent">
<LogOutIcon className="size-4" />
Log out
</button>
</div>
)}
</div>
)
}Checkout
tsx
import { useState } from "react"
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@polyui/react/collapsible"
import { Button } from "@polyui/react/button"
import { Card } from "@polyui/react/card"
import { Label } from "@polyui/react/label"
import { ChevronDownIcon } from "lucide-react"
export function CollapsibleCheckout() {
const [openSection, setOpenSection] = useState<string>("address")
const sections = [
{
id: "address",
title: "Delivery Address",
content: (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs">First name</Label>
<input
className="w-full rounded-md border px-3 py-1.5 text-sm outline-none focus:ring-2 focus:ring-ring"
placeholder="John"
/>
</div>
<div className="space-y-1">
<Label className="text-xs">Last name</Label>
<input
className="w-full rounded-md border px-3 py-1.5 text-sm outline-none focus:ring-2 focus:ring-ring"
placeholder="Doe"
/>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs">Address</Label>
<input
className="w-full rounded-md border px-3 py-1.5 text-sm outline-none focus:ring-2 focus:ring-ring"
placeholder="123 Main St"
/>
</div>
<Button size="sm" onClick={() => setOpenSection("delivery")}>
Continue
</Button>
</div>
),
},
{
id: "delivery",
title: "Delivery Options",
content: (
<div className="space-y-3">
{["Standard (3-5 days) — Free", "Express (1-2 days) — $9.99", "Same day — $19.99"].map((opt) => (
<label key={opt} className="flex cursor-pointer items-center gap-2">
<input type="radio" name="delivery" className="size-4" />
<span className="text-sm">{opt}</span>
</label>
))}
<Button size="sm" onClick={() => setOpenSection("payment")}>
Continue
</Button>
</div>
),
},
{
id: "payment",
title: "Payment",
content: (
<div className="space-y-3">
<div className="space-y-1">
<Label className="text-xs">Card number</Label>
<input
className="w-full rounded-md border px-3 py-1.5 text-sm outline-none focus:ring-2 focus:ring-ring"
placeholder="4242 4242 4242 4242"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label className="text-xs">Expiry</Label>
<input
className="w-full rounded-md border px-3 py-1.5 text-sm outline-none focus:ring-2 focus:ring-ring"
placeholder="MM/YY"
/>
</div>
<div className="space-y-1">
<Label className="text-xs">CVC</Label>
<input
className="w-full rounded-md border px-3 py-1.5 text-sm outline-none focus:ring-2 focus:ring-ring"
placeholder="123"
/>
</div>
</div>
<Button size="sm">Place Order</Button>
</div>
),
},
]
return (
<div className="w-full max-w-sm space-y-2">
{sections.map((section, i) => (
<Collapsible
key={section.id}
open={openSection === section.id}
onOpenChange={(open) => open && setOpenSection(section.id)}
>
<CollapsibleTrigger className="flex w-full items-center justify-between rounded-md border px-4 py-3 text-sm font-medium hover:bg-accent">
<span className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground">
{i + 1}
</span>
{section.title}
</span>
<ChevronDownIcon
className={`size-4 transition-transform ${openSection === section.id ? "rotate-180" : ""}`}
/>
</CollapsibleTrigger>
<CollapsibleContent className="rounded-md border border-t-0 px-4 py-3">{section.content}</CollapsibleContent>
</Collapsible>
))}
</div>
)
}Animated
@peduarte starred 3 repositories
@radix-ui/primitives
tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@polyui/react/collapsible"
import { Button } from "@polyui/react/button"
import { ChevronsUpDownIcon } from "lucide-react"
export function CollapsibleAnimated() {
return (
<Collapsible className="flex w-full max-w-[350px] flex-col gap-2">
<div className="flex items-center justify-between gap-4 px-4">
<div className="text-sm font-semibold">@peduarte starred 3 repositories</div>
<CollapsibleTrigger render={<Button variant="ghost" size="icon-sm" />}>
<ChevronsUpDownIcon className="size-4" />
<span className="sr-only">Toggle</span>
</CollapsibleTrigger>
</div>
<div className="rounded-md border px-4 py-2 font-mono text-sm">@radix-ui/primitives</div>
<CollapsibleContent className="data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down flex flex-col gap-2 overflow-hidden transition-all duration-300">
<div className="rounded-md border px-4 py-2 font-mono text-sm">@radix-ui/colors</div>
<div className="rounded-md border px-4 py-2 font-mono text-sm">@stitches/react</div>
</CollapsibleContent>
</Collapsible>
)
}Props
Collapsible
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
open | boolean | — | Controlled open state. |
onOpenChange | (open: boolean) => void | — | Callback when open state changes. |
defaultOpen | boolean | false | Uncontrolled initial open state. |
disabled | boolean | false | Disable expand/collapse interaction. |
CollapsibleContent
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
className | string | — | Custom class for the content area. |