Qure UI
Components

Collapsible

One section that opens and closes.

'use client'import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/registry/qure/ui/collapsible'export default function CollapsibleDemo() {  return (    <Collapsible className="max-w-md">      <CollapsibleTrigger>Prior studies (3)</CollapsibleTrigger>      <CollapsibleContent>        <ul className="space-y-1">          <li>CT Thorax — 14 March 2025 — Dr A. Menon</li>          <li>CXR PA — 2 January 2025 — Dr S. Iyer</li>          <li>CXR PA — 11 September 2024 — Dr S. Iyer</li>        </ul>      </CollapsibleContent>    </Collapsible>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/collapsible.json

Usage

import {
  Collapsible, CollapsibleContent, CollapsibleTrigger,
} from '@/components/ui/collapsible'
<Collapsible>
  <CollapsibleTrigger>Prior studies (3)</CollapsibleTrigger>
  <CollapsibleContent>CT Thorax — 14 March 2025</CollapsibleContent>
</Collapsible>

Reach for Accordion when there are several sections and their open states are related — one at a time, or a set that shares a keyboard ring. Reach for Collapsible when there is one section, or several that happen to sit near each other and know nothing about one another. It has no value, no roving focus and no header element to argue with.

Basic

'use client'import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/registry/qure/ui/collapsible'export default function CollapsibleDemo() {  return (    <Collapsible className="max-w-md">      <CollapsibleTrigger>Prior studies (3)</CollapsibleTrigger>      <CollapsibleContent>        <ul className="space-y-1">          <li>CT Thorax — 14 March 2025 — Dr A. Menon</li>          <li>CXR PA — 2 January 2025 — Dr S. Iyer</li>          <li>CXR PA — 11 September 2024 — Dr S. Iyer</li>        </ul>      </CollapsibleContent>    </Collapsible>  )}

Closed by default. Pass defaultOpen for the other way round.

Controlled

'use client'import * as React from 'react'import { Badge } from '@/registry/qure/ui/badge'import { Button } from '@/registry/qure/ui/button'import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/registry/qure/ui/collapsible'export default function CollapsibleControlled() {  const [open, setOpen] = React.useState(false)  return (    <div className="w-full max-w-md space-y-3">      <Button size="sm" variant="tertiary" onClick={() => setOpen(o => !o)}>        {open ? 'Hide' : 'Show'} AI findings      </Button>      <Collapsible open={open} onOpenChange={setOpen}>        <CollapsibleTrigger>          <span className="flex items-center gap-2">            qXR findings            <Badge tone="attention" size="sm">2 abnormal</Badge>          </span>        </CollapsibleTrigger>        <CollapsibleContent>          <ul className="space-y-1">            <li>Nodule — right upper lobe — confidence 0.86</li>            <li>Pleural effusion — left — confidence 0.61</li>          </ul>        </CollapsibleContent>      </Collapsible>    </div>  )}

open with onOpenChange when something outside the disclosure has to open it — a "show all" control, a deep link into a finding, a keyboard shortcut.

Report sections

Non-contrast volumetric acquisition of the thorax, 1mm slices, reconstructed in axial, coronal and sagittal planes.
'use client'import { Card, CardContent } from '@/registry/qure/ui/card'import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/registry/qure/ui/collapsible'export default function CollapsibleCard() {  return (    <Card className="w-full max-w-md">      <CardContent>        <Collapsible defaultOpen>          <CollapsibleTrigger className="justify-between">Technique</CollapsibleTrigger>          <CollapsibleContent>            Non-contrast volumetric acquisition of the thorax, 1mm slices, reconstructed in            axial, coronal and sagittal planes.          </CollapsibleContent>        </Collapsible>        {/* hiddenUntilFound keeps the text in the DOM so Ctrl+F finds it and            the browser opens the section on its own. */}        <Collapsible>          <CollapsibleTrigger className="justify-between">Comparison</CollapsibleTrigger>          <CollapsibleContent hiddenUntilFound>            CT Thorax of 14 March 2025 and CXR of 2 January 2025.          </CollapsibleContent>        </Collapsible>        <Collapsible>          <CollapsibleTrigger className="justify-between">Impression</CollapsibleTrigger>          <CollapsibleContent>            Stable 8mm right upper lobe nodule. No new focal lesion. Routine follow-up at            twelve months.          </CollapsibleContent>        </Collapsible>      </CardContent>    </Card>  )}

Three independent disclosures rather than an accordion: a radiologist reading Technique does not want Impression to shut. The middle one uses hiddenUntilFound, so the browser's own find can reach text inside a closed section and open it — worth having wherever the hidden content is prose someone might Ctrl+F for.

In a sidebar

'use client'import { ChevronDownIcon } from 'lucide-react'import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/registry/qure/ui/collapsible'const groups = [  { label: 'Worklist', items: ['Unread (24)', 'In progress (3)', 'Awaiting sign-off (7)'] },  { label: 'Modalities', items: ['CXR', 'CT Thorax', 'CT Head'] },]export default function CollapsibleSidebar() {  return (    <nav className="w-56 space-y-1">      {groups.map((group, i) => (        <Collapsible key={group.label} defaultOpen={i === 0}>          {/* showChevron={false} drops the trailing chevron so a leading one              can take its place — a sidebar reads left to right. Giving it              cn-collapsible-icon is what makes it turn with the panel. */}          <CollapsibleTrigger showChevron={false} className="w-full text-sm">            <ChevronDownIcon className="cn-collapsible-icon size-4 shrink-0" />            {group.label}          </CollapsibleTrigger>          <CollapsibleContent>            <ul className="list-none space-y-1 pl-6">              {group.items.map(item => (                <li key={item}>{item}</li>              ))}            </ul>          </CollapsibleContent>        </Collapsible>      ))}    </nav>  )}

showChevron={false} drops the trailing chevron so a leading one can take its place. Give your own icon the cn-collapsible-icon class and it turns with the panel — the rotation lives in the stylesheet, keyed off data-panel-open:

<CollapsibleTrigger showChevron={false}>
  <ChevronDownIcon className="cn-collapsible-icon size-4" />
  Worklist
</CollapsibleTrigger>

data-panel-open lives on the trigger. The panel's own data-open is on a different element, and styling against it from the trigger fails silently — the section opens and the chevron does not move. It cost an hour here.

How it animates

Base UI measures the panel and publishes its height as --collapsible-panel-height, so content animates from nothing to its natural size with no hardcoded number:

.cn-collapsible-content {
  height: var(--collapsible-panel-height);
  transition: height 200ms var(--ease-qure);
}
.cn-collapsible-content[data-starting-style] { height: 0; }

The padding lives on an inner div rather than on the panel, because a panel with padding cannot animate to zero height. Disabled under prefers-reduced-motion.

API Reference

Everything Base UI's Collapsible accepts, plus one prop of ours on the trigger.

Prop

Type

On this page