Skip to main content
PolyUI/docs

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/react

Basic

@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
  • 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>
  )
}

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

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

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

属性类型默认值说明
openbooleanControlled open state.
onOpenChange(open: boolean) => voidCallback when open state changes.
defaultOpenbooleanfalseUncontrolled initial open state.
disabledbooleanfalseDisable expand/collapse interaction.

CollapsibleContent

属性类型默认值说明
classNamestringCustom class for the content area.