展示组件
Collapsible 折叠面板
可展开/收起的内容区域,支持受控与非受控模式,内置过渡动画。
安装
bash
pnpm dlx shadcn@latest add collapsible -c packages/react基础
@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>
)
}文件树
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>
)
}显示更多
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>
)
}资料列表
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>
)
}筛选
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>
)
}常见问题
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>
)
}卡片
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>
)
}嵌套菜单
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>
)
}结账
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>
)
}动画效果
@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 | — | 受控展开状态。 |
onOpenChange | (open: boolean) => void | — | 展开状态变化回调。 |
defaultOpen | boolean | false | 非受控初始展开状态。 |
disabled | boolean | false | 禁用展开/收起交互。 |
CollapsibleContent
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
className | string | — | 内容区域的自定义样式。 |