Vela UI Docsvenizia.ai

Collapsible

A headless show-and-hide panel that animates on its measured height.

npx shadcn@latest add @velaui/collapsible
npx shadcn@latest add @velaui/collapsible

Examples

  • Default

View code

Collapsible · Default

Installation

pnpm dlx velaui add @velaui/collapsible
pnpm dlx velaui add @velaui/collapsible

Code

collapsible-default.tsx
import * as React from "react";

import { ChevronDown } from "@/components/icons/chevron-down";
import { Button } from "@/components/ui/button";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

export default function Example() {
  const [open, setOpen] = React.useState(false);

  return (
    <Collapsible open={open} onOpenChange={setOpen} className="w-full max-w-sm">
      <div className="flex items-center justify-between gap-4">
        <p className="text-sm font-medium text-text-strong">
          3 repositories starred
        </p>
        <CollapsibleTrigger asChild>
          <Button variant="ghost" size="icon-sm" aria-label="Toggle list">
            <ChevronDown
              className={open ? "rotate-180 transition-transform" : "transition-transform"}
            />
          </Button>
        </CollapsibleTrigger>
      </div>
      <div className="mt-2 rounded-lg border border-border-default px-3 py-2 text-sm">
        venizia/ignis
      </div>
      <CollapsibleContent className="flex flex-col gap-2 pt-2">
        <div className="rounded-lg border border-border-default px-3 py-2 text-sm">
          venizia/velaui
        </div>
        <div className="rounded-lg border border-border-default px-3 py-2 text-sm">
          venizia/ardor
        </div>
      </CollapsibleContent>
    </Collapsible>
  );
}
import * as React from "react";

import { ChevronDown } from "@/components/icons/chevron-down";
import { Button } from "@/components/ui/button";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

export default function Example() {
  const [open, setOpen] = React.useState(false);

  return (
    <Collapsible open={open} onOpenChange={setOpen} className="w-full max-w-sm">
      <div className="flex items-center justify-between gap-4">
        <p className="text-sm font-medium text-text-strong">
          3 repositories starred
        </p>
        <CollapsibleTrigger
          render={
            <Button variant="ghost" size="icon-sm" aria-label="Toggle list">
              <ChevronDown
                className={
                  open
                    ? "rotate-180 transition-transform"
                    : "transition-transform"
                }
              />
            </Button>
          }
        />
      </div>
      <div className="mt-2 rounded-lg border border-border-default px-3 py-2 text-sm">
        venizia/ignis
      </div>
      <CollapsibleContent className="flex flex-col gap-2 pt-2">
        <div className="rounded-lg border border-border-default px-3 py-2 text-sm">
          venizia/velaui
        </div>
        <div className="rounded-lg border border-border-default px-3 py-2 text-sm">
          venizia/ardor
        </div>
      </CollapsibleContent>
    </Collapsible>
  );
}

Usage

import * as React from "react";

import { ChevronDown } from "@/components/icons/chevron-down";
import { Button } from "@/components/ui/button";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

export default function Example() {
  const [open, setOpen] = React.useState(false);

  return (
    <Collapsible open={open} onOpenChange={setOpen} className="w-full max-w-sm">
      <div className="flex items-center justify-between gap-4">
        <p className="text-sm font-medium text-text-strong">
          3 repositories starred
        </p>
        <CollapsibleTrigger asChild>
          <Button variant="ghost" size="icon-sm" aria-label="Toggle list">
            <ChevronDown
              className={open ? "rotate-180 transition-transform" : "transition-transform"}
            />
          </Button>
        </CollapsibleTrigger>
      </div>
      <div className="mt-2 rounded-lg border border-border-default px-3 py-2 text-sm">
        venizia/ignis
      </div>
      <CollapsibleContent className="flex flex-col gap-2 pt-2">
        <div className="rounded-lg border border-border-default px-3 py-2 text-sm">
          venizia/velaui
        </div>
        <div className="rounded-lg border border-border-default px-3 py-2 text-sm">
          venizia/ardor
        </div>
      </CollapsibleContent>
    </Collapsible>
  );
}
import * as React from "react";

import { ChevronDown } from "@/components/icons/chevron-down";
import { Button } from "@/components/ui/button";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";

export default function Example() {
  const [open, setOpen] = React.useState(false);

  return (
    <Collapsible open={open} onOpenChange={setOpen} className="w-full max-w-sm">
      <div className="flex items-center justify-between gap-4">
        <p className="text-sm font-medium text-text-strong">
          3 repositories starred
        </p>
        <CollapsibleTrigger
          render={
            <Button variant="ghost" size="icon-sm" aria-label="Toggle list">
              <ChevronDown
                className={
                  open
                    ? "rotate-180 transition-transform"
                    : "transition-transform"
                }
              />
            </Button>
          }
        />
      </div>
      <div className="mt-2 rounded-lg border border-border-default px-3 py-2 text-sm">
        venizia/ignis
      </div>
      <CollapsibleContent className="flex flex-col gap-2 pt-2">
        <div className="rounded-lg border border-border-default px-3 py-2 text-sm">
          venizia/velaui
        </div>
        <div className="rounded-lg border border-border-default px-3 py-2 text-sm">
          venizia/ardor
        </div>
      </CollapsibleContent>
    </Collapsible>
  );
}

Props

Collapsible

Takes every prop of CollapsibleProps.

CollapsibleContent

Takes every prop of CollapsibleContentProps.

CollapsibleTrigger

Takes every prop of CollapsibleTriggerProps.

Dependencies

npm
radix-ui@1.6.0
Installs with it
nothing else
npm
@base-ui/react@1.6.0
Installs with it
nothing else

Source

components/ui/collapsible.tsx
import { Collapsible as CollapsiblePrimitive } from "radix-ui";

import { cn } from "@/lib/utils";

export type CollapsibleProps = React.ComponentProps<typeof CollapsiblePrimitive.Root>;

/**
 * Collapsible — a headless show/hide primitive. It carries no design of its
 * own; the trigger and the panel are whatever the caller renders inside it.
 * The content animates on its measured height, which Radix publishes as
 * `--radix-collapsible-content-height`.
 */
function Collapsible({
  ...props
}: CollapsibleProps) {
  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}

export type CollapsibleTriggerProps = React.ComponentProps<typeof CollapsiblePrimitive.Trigger>;

function CollapsibleTrigger({
  ...props
}: CollapsibleTriggerProps) {
  return (
    <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
  );
}

export type CollapsibleContentProps = React.ComponentProps<typeof CollapsiblePrimitive.Content>;

function CollapsibleContent({
  className,
  ...props
}: CollapsibleContentProps) {
  return (
    <CollapsiblePrimitive.Content
      data-slot="collapsible-content"
      className={cn(
        "overflow-hidden data-open:animate-collapsible-down data-closed:animate-collapsible-up",
        className,
      )}
      {...props}
    />
  );
}

export { Collapsible, CollapsibleContent, CollapsibleTrigger };
components/ui/collapsible.tsx
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";

import { cn } from "@/lib/utils";

export type CollapsibleProps = React.ComponentProps<typeof CollapsiblePrimitive.Root>;

/**
 * Collapsible — a headless show/hide primitive. It carries no design of its
 * own; the trigger and the panel are whatever the caller renders inside it.
 * The content animates on its measured height, which Base UI publishes as
 * `--collapsible-panel-height`.
 */
function Collapsible({
  ...props
}: CollapsibleProps) {
  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
}

export type CollapsibleTriggerProps = React.ComponentProps<typeof CollapsiblePrimitive.Trigger>;

function CollapsibleTrigger({
  ...props
}: CollapsibleTriggerProps) {
  return (
    <CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
  );
}

export type CollapsibleContentProps = React.ComponentProps<typeof CollapsiblePrimitive.Panel>;

function CollapsibleContent({
  className,
  ...props
}: CollapsibleContentProps) {
  return (
    <CollapsiblePrimitive.Panel
      data-slot="collapsible-content"
      className={cn(
        "h-(--collapsible-panel-height) overflow-hidden transition-[height] duration-200 ease-out data-starting-style:h-0 data-ending-style:h-0",
        className,
      )}
      {...props}
    />
  );
}

export { Collapsible, CollapsibleContent, CollapsibleTrigger };