CollapseDesign system
ComponentProduct

Collapse

Expandable content with height and opacity transitions.

npx context-ds add collapse
In the product

Usage

Use it for

Advanced settings, optional fields and details shown on demand.

Avoid

  • Hiding content people need to finish a task

Example

examples/collapse.tsx
"use client";import { useState } from "react";import { Button } from "@/components/ui/button";import { Collapse } from "@/components/ui/collapse";import { Field } from "@/components/ui/field";import { Input } from "@/components/ui/input";export function Product() {  const [open, setOpen] = useState(true);  return (    <div className="flex w-full max-w-sm flex-col gap-3">      <Button variant="ghost" size="sm" className="self-start" aria-expanded={open} aria-controls="advanced" onClick={() => setOpen((o) => !o)}>        {open ? "Hide advanced settings" : "Show advanced settings"}      </Button>      <Collapse open={open} id="advanced" className="flex flex-col gap-4 pt-1">        <Field label="Timeout" htmlFor="timeout" help="Seconds before a request gives up.">          <Input id="timeout" defaultValue="30" />        </Field>      </Collapse>    </div>  );}

Source

components/ui/collapse.tsx
"use client";import * as React from "react";import { cn } from "@/lib/utils";/** * Grows from nothing to its content's height and back (300ms ease-out on the grid's rows, with a fade), so whatever * sits below glides rather than jumps. Stays mounted while closed (inert, so nothing inside takes focus), and clips only * until it has fully opened, so menus inside can still spill out of it. Reduced motion opens and closes at once. */function Collapse({ open, id, className, children }: { open: boolean; id?: string; className?: string; children: React.ReactNode }) {  const [isFullyOpen, setFullyOpen] = React.useState(open);  if (!open && isFullyOpen) setFullyOpen(false);  const handleTransitionEnd = (event: React.TransitionEvent<HTMLDivElement>) => {    if (open && event.target === event.currentTarget && event.propertyName === "grid-template-rows") setFullyOpen(true);  };  return (    <div      id={id}      data-slot="collapse"      data-state={open ? "open" : "closed"}      inert={!open}      onTransitionEnd={handleTransitionEnd}      className={cn("grid transition-[grid-template-rows,opacity] duration-300 ease-out motion-reduce:transition-none", open ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0")}    >      <div className={cn("min-h-0", !isFullyOpen && "overflow-hidden", open && "motion-reduce:overflow-visible")}>        <div className={className}>{children}</div>      </div>    </div>  );}export { Collapse };

Reference: src/components/dashboard-ui/collapse.tsx.