Collapsible
A single disclosure that animates open and closed — built on Base UI's Collapsible primitive with four variants, five sizes, rich trigger slots, tone-aware theming and full RTL support.
Playground
Installation
pnpm add @tessinaui/uiUsage
import {
CollapsibleRoot,
CollapsibleHeader,
CollapsibleTrigger,
CollapsiblePanel,
} from "@tessinaui/ui";<CollapsibleRoot variant="outline">
<CollapsibleHeader>
<CollapsibleTrigger>Show advanced options</CollapsibleTrigger>
</CollapsibleHeader>
<CollapsiblePanel>
Tweak request timeouts, retry policy, and SDK telemetry — most teams leave
these on the defaults.
</CollapsiblePanel>
</CollapsibleRoot>Examples
Default
A basic disclosure with the outline variant, closed by default.
Variants
Four visual treatments — ghost, outline, filled, and elevated.
Sizes
Five sizes from xs to xl scale the trigger and panel together.
Indicators
Built-in indicator sets — chevron, plus, caret, none — plus the
contained style and the start side. All three knobs live on the root.
Rich trigger
leadingIcon, description and trailingText slots, with the header
rendered as a real heading via headingLevel. The slots reach the button
through aria-describedby — the accessible name stays the title alone.
Order summary
The checkout idiom: trailingText keeps the total visible while collapsed,
the breakdown reveals on demand.
Header actions
Actions are siblings of the trigger inside CollapsibleHeader — never
children of the button. Set fullWidth={false} so the trigger shares the row.
FAQ list
Independent disclosures in a divider list — several stay open at once, and long questions wrap rather than truncate. For a single-open group with arrow keys, use Accordion.
Nested
caret carries hierarchy semantics (leading, points along the reading
direction closed, down open — the HIG disclosure triangle). Rotation state is
scoped per disclosure, so levels never leak into each other.
Form panel
A controlled disclosure hosting real form controls. onOpenChangeComplete
fires when the height animation settles — the moment to focus a revealed
control or lazy-load panel data.
Open emphasis
Style the open state through Base UI's data attributes — data-open on the
root, data-panel-open on the trigger — no extra prop needed.
On color
tone="on-color" derives ink, borders, washes and the indicator from the
surface ink published by the plate underneath.
Intents
A subtle border accent for status context on non-ghost variants.
Loading
Skeleton placeholders mirror the collapsed and open layouts. label takes
the real title's character count so the bar measures what the title measures.
When to use
Reach for Collapsible whenever you have a single block of secondary content
that should be hidden by default and revealed on demand — advanced form
options, an FAQ row, an order-summary breakdown, an expandable list row. Keep
the trigger label short and descriptive (users decide whether to expand from
the label alone), and never hide content most users need.
Two look-alikes are different components: a row that navigates somewhere
points its chevron along the reading direction and belongs to a list or menu —
a disclosure's chevron points down and reveals in place. And multiple related
disclosures where only one should stay open belong to
Accordion, which adds arrow-key movement and
the single-or-multiple-open semantic.
API Reference
CollapsibleRoot props
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Scales trigger padding, text, indicator, and panel padding |
variant | "ghost" | "outline" | "filled" | "elevated" | "ghost" | Visual treatment — ghost is just trigger + panel, the others wrap everything in a shell |
rounded | "none" | "sm" | "md" | "lg" | "full" | "lg" | §2 panel map (full caps at rounded-3xl). Rounds the shell — or the ghost trigger itself |
intent | "none" | "primary" | "error" | "warning" | "success" | "info" | "none" | Border accent on non-ghost shells. No-op on ghost |
indicator | "chevron" | "plus" | "caret" | "none" | "chevron" | Trigger indicator set. caret = leading hierarchy semantics |
indicatorStyle | "contained" | "plain" | "plain" | Bare glyph, or a tinted circle behind it |
iconSide | "start" | "end" | "end" | Which edge the indicator sits on (logical — mirrors in RTL) |
tone | "default" | "on-color" | "default" | on-color derives chrome from the surface ink |
dir | "ltr" | "rtl" | — | No default: direction inherits from <html dir>; set only to override |
open / defaultOpen / onOpenChange | — | — | Controlled / uncontrolled state. onOpenChange(open, eventDetails) — eventDetails carries the reason and a cancel() |
onOpenChangeComplete | (open: boolean) => void | — | Fires when the height animation settles (immediately under reduced motion) |
disabled | boolean | false | Disables the trigger and prevents toggling |
render | ReactElement | (props, state) => ReactElement | — | Base UI polymorphic prop (equivalent to Radix asChild) |
CollapsibleHeader props
A flex row for the trigger plus any sibling actions.
| Prop | Type | Default | Description |
|---|---|---|---|
headingLevel | 2 | 3 | 4 | 5 | 6 | — | Renders the header as a real <h2>–<h6> so the disclosure joins the page outline. Keep actions outside a heading header |
CollapsibleTrigger props
| Prop | Type | Default | Description |
|---|---|---|---|
leadingIcon | ReactNode | — | Decorative media before the title — kept out of the accessible name |
description | ReactNode | — | Supporting line under the title, associated via aria-describedby |
trailingText | ReactNode | — | Meta before the indicator (value, count, price) — also aria-describedby |
customIndicator | ReactNode | — | Replaces the built-in indicator glyph |
fullWidth | boolean | true | Stretch the trigger to fill its container |
render / nativeButton | — | — | Base UI polymorphism — render as a different element while preserving trigger behaviour |
Renders a <button> with aria-expanded and aria-controls (APG
disclosure). The title wraps rather than truncating. Keyboard: Enter and
Space toggle; a lone disclosure has no arrow-key behaviour by design.
Row layout. trailingText sits on the title's line, baseline-aligned,
and the description takes the full text column beneath both — so a long
description reflows in the whole width rather than in whatever the value left
over. The indicator is chrome outside that block: pinned to the trigger's
trailing edge and optically centred over however many lines the row grows to.
That is the dominant phone convention (TES-72 surveyed 44 apps: 13 of the 16
carrying a real trailing value keep it on the title's line, none move it above
the title, and none truncate the title to protect it). Keep trailingText
short — a value, a count, a price — and put anything longer in description.
CollapsiblePanel props
| Prop | Type | Default | Description |
|---|---|---|---|
keepMounted | boolean | false | Keep the panel in the DOM while closed. Ignored when hiddenUntilFound is set |
hiddenUntilFound | boolean | false | hidden="until-found" — closed content stays reachable by find-in-page; a browser match reveals the panel one-way and state syncs through onOpenChange |
withPadding | boolean | true | Size-scaled panel padding (set false when wrapping a Card) |
render | ReactElement | (props, state) => ReactElement | — | Base UI polymorphic prop |
CollapsibleSkeleton props
| Prop | Type | Default | Description |
|---|---|---|---|
variant / size / rounded / iconSide / indicatorStyle | — | component defaults | Mirrors the real component's frame — same class maps, touch floor included |
open | boolean | false | Render the open-panel placeholder |
lines | number | 3 | Body text lines in the open panel |
label | number | 20 | Character count of the real title — the bar measures what the title measures |
showIndicator | boolean | true | Render the indicator placeholder |
Notes
- Built on
@base-ui/react/collapsible. State attributes for styling and testing:data-open/data-closed/data-starting-style/data-ending-styleon root and panel,data-panel-openon the trigger,data-disabledeverywhere while disabled. Every part also carries a stabledata-slot(collapsible-root,-header,-trigger,-trigger-title,-trigger-description,-trigger-meta,-trigger-icon,-indicator,-panel,-panel-content,-skeleton) for agents and tests. - The animation transitions
heightbetween0andvar(--collapsible-panel-height)— any content height, no measurement, and it ismotion-safe-gated so reduced motion gets an instant toggle. hiddenUntilFoundneeds the panel to participate in layout — never style the paneldisplay: none/contentsyourself. Safari support forhidden="until-found"is recent (late 2025); older Safari degrades to hidden-until-opened with no find-in-page reveal.- For multi-item disclosures, prefer Accordion. For floating revealed content, prefer Popover or HoverCard. For clamped "show more" text, a dedicated spoiler component is tracked in TES-187.