Collapsible

One section that shows and hides.

Example

Installation

pnpm dlx shadcn@latest add superzero11/uiset/collapsible

It lands in components/ui/collapsible.tsx, with UISet’s root (uiset). Wrap your app in <UISet> once, and every component takes its skin.

What UISet adds

The component is UI SDK’s own, with its parts, props and names, so everything below works as UI SDK documents it. UISet adds the look and checks it:

  • Styles in five skins, each in light and dark, from CSS variables you can override.
  • Text at 4.5:1 contrast and control edges and focus rings at 3:1, in every skin and theme, checked with axe in tests.
  • Haptic cues on phones, and sound cues when you set sound on <UISet>.
  • Examples that name every control, so agents can find it (see Agents below).

The API reference and accessibility notes below are UI SDK’s, from its docs for @ui-sdk/react (Apache-2.0). Collapsible on ui-sdk.dev has more usage examples. Your coding agent can read the same page at node_modules/@ui-sdk/react/docs/components/collapsible.md.

API Reference

Props and callbacks are the same as below, as React props.

Collapsible

  • open? (boolean): Whether the panel is open. Pass it to control the collapsible.
  • defaultOpen? (boolean): Whether the panel is open at first, when uncontrolled.
  • onOpenChange? (((open: boolean, details: ChangeDetails<CollapsibleOpenChangeReason>) => void)): Runs when the panel wants to open or close. details.reason says why, and details.cancel() stops it.
  • onOpenChangeComplete? (((open: boolean) => void)): Runs once the panel has finished opening or closing, after any animation.
  • disabled? (boolean): Stops the trigger from opening or closing the panel. The trigger stays focusable.
  • controller? (object): Opens and closes the panel from anywhere. Create one with createCollapsibleController().
  • debug? (boolean): Logs every state change with its reason to the console, in development builds.

CollapsibleTrigger

The trigger takes no options of its own, only the props of the element it renders.

CollapsiblePanel

  • keepMounted? (boolean): Whether the panel stays in the page, hidden, while closed.
  • hiddenUntilFound? (boolean): Keeps the closed panel in the page as hidden="until-found", so the browser's find in page can match its text and open it.

State

What render functions, state-based class names and slots receive. The trigger and the panel receive the same state as the collapsible.

  • open (boolean): Whether the panel is open.
  • disabled (boolean): Whether the trigger ignores presses, by its own disabled or a disabled fieldset around it. The open option and the controller can still open and close it.

Reasons

details.reason says why the panel opened or closed.

  • trigger-press (MouseEvent | KeyboardEvent | TouchEvent): Click, tap, Enter or Space on the part that opens, closes or toggles. Arrow keys that open a menu or submenu count too. A tap on a combobox's input that opens a sheet reports its touchend.
  • find-in-page (Event): The browser's find in page matched hidden content.
  • imperative (none): A controller or api call, or a property set on a web component.

Controller

Create one with createCollapsibleController() and pass it as controller.

Every write method takes an optional { reason, event }, and returns false when the change was canceled or changed nothing.

  • isOpen() (() => boolean): Whether the panel is open.
  • setOpen(open, options?) ((open: boolean, options?: CollapsibleChangeOptions | undefined) => boolean): Opens or closes the panel. Returns false when the change was canceled or not possible.
  • open(options?) ((options?: CollapsibleChangeOptions | undefined) => boolean): Opens the panel.
  • close(options?) ((options?: CollapsibleChangeOptions | undefined) => boolean): Closes the panel.
  • toggle(options?) ((options?: CollapsibleChangeOptions | undefined) => boolean): Opens the panel when it's closed, and closes it when it's open.
  • focus() (() => void): Moves focus to the trigger.

Events

Callbacks get the new state first, then the details.

PropOnReceives
onOpenChangeCollapsible(open, details): the new state, and the reason
onOpenChangeCompleteCollapsible(open), after the panel's animation

Accessibility

The collapsible follows the WAI-ARIA disclosure pattern. The trigger says whether the panel is open with aria-expanded, and points to the panel with aria-controls while it's mounted. Every line below is an automated test.

Keyboard

KeyDoes
Enter, SpaceOpens or closes the panel. Focus stays on the trigger.
TabMoves from the trigger into an open panel

Closing a panel while focus is inside it moves focus back to the trigger.

Tested Behavior

Opening and closing

  • Starts closed, with the panel out of the page.
  • Clicking the trigger opens the panel, and clicking again closes it.
  • The trigger controls the panel.
  • Generated ids start with ui-sdk-.
  • It can start open.
  • An opening can be canceled.
  • A disabled collapsible doesn't open.
  • A disabled trigger stays focusable and says it's disabled.
  • A controller opens and closes it from anywhere.

Keyboard

  • Enter toggles the panel and keeps focus on the trigger.
  • Space toggles the panel and keeps focus on the trigger.
  • Tab moves from the open trigger into the panel.
  • Tab skips a closed panel.

Mounting

  • With keepMounted, the closed panel stays in the page, hidden.
  • With keepMounted, a class that sets display doesn't show the closed panel.
  • With hiddenUntilFound, the closed panel stays findable.
  • With hiddenUntilFound, a class that sets display keeps the closed panel findable.
  • Finding text in the closed panel opens it.
  • Following a link into the closed panel opens it.
  • The browser's own find opens the closed panel.

Runs in React, Vue and web components.

Beyond the pattern's baseline:

Enhancements

  • Closing the panel while focus is inside it moves focus to the trigger.
  • OnOpenChangeComplete runs once the animation has finished.
  • Find in page reports its own reason and opens without an animation.
  • A canceled find in page keeps the panel hidden until found.
  • An infinite exit animation still unmounts the panel.
  • Parts report open and closed through data attributes.

Runs in React, Vue and web components.

Screen readers hear the same thing in each tested framework, recorded by a virtual screen reader:

  • Announcements while using the collapsible with a keyboard.

Runs in React, Vue and web components.

Agents

UI SDK’s agent bridge finds this component by its accessible name: give it a visible label or an aria-label, and an agent can read its state and call its actions without screenshots. Building with AI agents shows how to connect one.

This is agent.snapshot() for the example above, read live from this page. Change something in the example, then read it again: the values follow.

agent.snapshot()
[
  {
    "ref": "c8",
    "component": "collapsible",
    "actions": [
      "setOpen",
      "press"
    ],
    "label": "Show advanced settings",
    "open": false
  }
]