Skip to main content
PolyUI/docs

展示组件

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
  • HL
    Howard Lloyd

    Product Manager

    90%
  • OS
    Olivia Sparks

    Software Engineer

    60%
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>
  )
}

卡片

How do I track my order?
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&apos;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

属性类型默认值说明
openboolean受控展开状态。
onOpenChange(open: boolean) => void展开状态变化回调。
defaultOpenbooleanfalse非受控初始展开状态。
disabledbooleanfalse禁用展开/收起交互。

CollapsibleContent

属性类型默认值说明
classNamestring内容区域的自定义样式。