DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
BreadcrumbTabsPaginationStepperMenu ItemToolbarMenubarNavigation MenuAccordionCollapsibleCommandBottom NavTop Header MobileTop Header DesktopSidebar
AI agentsContributing
  1. Components
  2. Collapsible

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.

$ pnpm add @tessinaui/ui

When to use

  • Secondary content hidden by default: advanced form options, an FAQ row, an order-summary breakdown, an expandable list row.
  • A single disclosure on its own. One block that opens in place, with no other disclosure it has to coordinate with.
  • A summary that stays visible. trailingText and description keep the total or the hint on the closed trigger, so people can decide whether to expand.
  • A short, descriptive trigger label. People decide whether to expand from the label alone, so name what is inside.
  • Independent rows in a list. Several stay open at once and long questions wrap rather than truncate (FAQ list).

When not to use

  • Content most people need. Never hide it behind a click; show it on the page.
  • A set where only one should stay open. Use Accordion, which adds arrow-key movement and the single-or-multiple-open semantic.
  • A row that navigates. Its chevron points along the reading direction and belongs to a list or menu; a disclosure's chevron points down and reveals in place.
  • Floating revealed content. Use Popover or HoverCard.
  • Views people switch between. Peer sections belong in Tabs, not in stacked disclosures.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import {
  CollapsibleRoot,
  CollapsibleHeader,
  CollapsibleTrigger,
  CollapsiblePanel,
} from "@tessinaui/ui";
example.tsx
<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. filled is a card for a grouped page, the same card Card, Fieldset and Form draw, so the example sits it on one.

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, and pass the root's rounded to the actions so their corners (and hover fill) follow the shell.

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

Inside a Surface, Collapsible inherits tone="on-color": ink, borders, washes and the indicator derive from the surface ink published by the plate underneath. Pass tone="on-color" yourself only on a plate something else paints; an explicit tone always wins.

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.

RTL

Set dir="rtl" and the indicator moves to the left edge. The default chevron points down and up, so it does not flip. The caret points along the reading direction while closed, so under RTL it points left. Leave dir unset and the disclosure follows the page.

API Reference

CollapsibleRoot props

PropTypeDefaultDescription
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"inheritedon-color derives chrome from the surface ink. Inherited from a wrapping <Surface>, else "default"; an explicit prop wins
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)
disabledbooleanfalseDisables the trigger and prevents toggling
renderReactElement | (props, state) => ReactElement—Base UI polymorphic prop (the asChild equivalent)

CollapsibleHeader props

A flex row for the trigger plus any sibling actions.

PropTypeDefaultDescription
headingLevel2 | 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

PropTypeDefaultDescription
leadingIconReactNode—Decorative media before the title — kept out of the accessible name
descriptionReactNode—Supporting line under the title, associated via aria-describedby
trailingTextReactNode—Meta before the indicator (value, count, price) — also aria-describedby
customIndicatorReactNode—Replaces the built-in indicator glyph
fullWidthbooleantrueStretch 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, plus aria-controls while the panel is open — Base UI omits it while closed, when the panel is not in the DOM (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 at the trailing edge, vertically centred on the whole text block — the same axis as leadingIcon and the indicator — while it fits beside the title and description. When it does not (typically on a phone, or at 200% text zoom), it wraps under the description, at the leading edge, and the description keeps the full width instead of being squeezed. The wrap is content-driven, never a breakpoint, and the value is never hidden. The indicator is chrome outside that block, pinned to the trigger's trailing edge. Keep trailingText short — a value, a count, a price — and put anything longer in description.

CollapsiblePanel props

PropTypeDefaultDescription
keepMountedbooleanfalseKeep the panel in the DOM while closed. Ignored when hiddenUntilFound is set
hiddenUntilFoundbooleanfalsehidden="until-found" — closed content stays reachable by find-in-page; a browser match reveals the panel one-way and state syncs through onOpenChange
withPaddingbooleantrueSize-scaled panel padding (set false when wrapping a Card). The panel clips for its height animation, so flush content keeps focus rings inside itself — a nested CollapsibleRoot does this for you
renderReactElement | (props, state) => ReactElement—Base UI polymorphic prop

CollapsibleSkeleton props

PropTypeDefaultDescription
variant / size / rounded / iconSide / indicatorStyle—component defaultsMirrors the real component's frame — same class maps, touch floor included
openbooleanfalseRender the open-panel placeholder
linesnumber3Body text lines in the open panel
labelnumber20Character count of the real title — the bar measures what the title measures
showIndicatorbooleantrueRender the indicator placeholder

Accessibility

Pattern: WAI-ARIA Disclosure.

  • A real button with its state. CollapsibleTrigger renders a <button> with aria-expanded (and aria-controls while open) from @base-ui/react/collapsible, so collapsed and expanded are announced without any work on your side.
  • The name is the title alone. description and trailingText reach the trigger through aria-describedby; leadingIcon and the indicator glyph are aria-hidden, since the state already rides on aria-expanded. Your own aria-labelledby / aria-describedby merge with these rather than replacing them.
  • Headings are opt-in. headingLevel on CollapsibleHeader wraps the trigger in a heading, so the disclosure joins the page outline where it should.
  • Header actions are siblings, never children, of the trigger — interactive content inside a <button> is unreachable (see the header-actions example).
  • Closed content can stay findable. hiddenUntilFound lets find-in-page reach a closed panel and reveal it; state syncs through onOpenChange.
  • Moving focus into revealed content belongs in onOpenChangeComplete, which fires once the height animation settles (immediately under reduced motion).
  • Contrast and touch — the focus ring is the house 2px ring on :focus-visible. On ghost (the default) it sits outside the trigger over a 2px gap; the shelled variants clip, so theirs is inset — and so is a ghost trigger nested in another collapsible's panel, which clips for its height animation. Supporting ink (description, meta) is stepped up on the hover wash, where the muted tier measured 4.34:1 in light; axe color-contrast is clean on the demo and playground in both themes. Triggers clear 44px on phones.
  • Motion — the height transition is motion-safe, so reduced motion gets an instant toggle.

Keyboard

KeyBehaviour
Tab / Shift+TabMoves to the trigger, then into the panel's content when open
Enter / SpaceToggles the panel
Arrow keysNothing — a lone disclosure has no arrow-key behaviour by design. For a set, use Accordion

AT matrix

ATStatus
VoiceOver + Chrome (macOS)Passed 2026-09-09 (headed Chromium) — the name stays the title alone, the aria-describedby slots are read, and collapsed → expanded is announced on VO+Space
VoiceOver + Safari (macOS)UNTESTED — Safari blocked the scripted route used for this smoke
NVDA + Chrome (Windows)UNTESTED — no Windows host available
JAWSUNTESTED

One NVDA pass is still owed under QA flow §5.6.

Notes

  • Built on @base-ui/react/collapsible. State attributes for styling and testing: data-open / data-closed / data-starting-style / data-ending-style on root and panel, data-panel-open on the trigger, data-disabled everywhere while disabled. Every part also carries a stable data-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 height between 0 and var(--collapsible-panel-height) — any content height, no measurement, and it is motion-safe-gated so reduced motion gets an instant toggle.
  • hiddenUntilFound needs the panel to participate in layout — never style the panel display: none/contents yourself. Safari support for hidden="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.
AccordionCommand

On this page

When to useWhen not to useInstallationUsageExamplesDefaultVariantsSizesIndicatorsRich triggerOrder summaryHeader actionsFAQ listNestedForm panelOpen emphasisOn colorIntentsLoadingRTLAPI ReferenceCollapsibleRoot propsCollapsibleHeader propsCollapsibleTrigger propsCollapsiblePanel propsCollapsibleSkeleton propsAccessibilityKeyboardAT matrixNotes