Components / disclosure
Collapsible
This is one of two components that look like they need a binding and do not — the other is RadioGroup. A component moves up a tier by taking a native element, never by adding script to the core.
Summary
One trigger, one region.
| APG pattern | Disclosure |
| Built on | Radix |
| Status | stable |
| Since | 0.2.0 |
Use it to take something off the screen that most readers do not need.
<Collapsible summary="Advanced">…</Collapsible>
Renamed from Disclosure in 0.2.0. The APG calls the pattern disclosure and it still does — that row above is unchanged. What moved is the component's own name, because disclosure names the behavior rather than the thing, and a reader who has not read the APG cannot guess what it draws. Collapsible is what the primitive underneath is called.
The anatomy moved with it: data-se="disclosure" is now data-se="collapsible". That is the breaking half — it is what a Vue or Astro consumer writes, and there is no alias.
Anatomy
A <details data-se="disclosure"> with a <summary> as the trigger.
| Part | Element | Required |
|---|---|---|
content | <radixcollapsible.content> | yes |
indicator | <svg> | yes |
trigger | <radixcollapsible.trigger> | yes |
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
label | React.ReactNode | required | The trigger's visible text. |
open | boolean | none | Passing this makes the disclosure controlled. Without it the native <details> keeps its own state and needs no JavaScript at all. |
defaultOpen | boolean | none | Starts open. Use it for a section most readers will want, rather than for one they must not miss — a required field inside a closed region is invisible. |
onOpenChange | (open: boolean) => void | none | Called when the reader toggles it. |
children | React.ReactNode | none | The region's contents. Revealed on open; still in the DOM when closed, so anything focusable inside is reachable — do not put a form control in a section the reader may never open. |
States
| State | Expressed as | |
|---|---|---|
| Focused | :focus-visible | |
| Hover | :hover | |
| Open or closed | data-state |
Behavior
It needs no script. The anatomy is <details>, so open and close come from the platform. Radix supplies animated height and nothing else.
Accessibility
<summary> is a button with aria-expanded, from the element. Enter and Space both work without a line of JavaScript.
Do not hide anything the reader must act on. A required field or an error inside a collapsed region is invisible, and no amount of styling fixes that.
Keyboard
| Key | Result |
|---|---|
Enter / Space | Toggles the region — from <summary>, with no script |
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-hidden | always true |
Focus
The summary is the only stop. When the region closes, focus stays on the summary rather than vanishing with the content.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It sets no target size of its own; anything interactive inside it brings its own.
Tested
test/browser/upstream-natives.spec.mjs
Known gaps
- 3 upstream refs — their image-description example
- 5 upstream refs — a navigation disclosure set — a composition we do not ship
Appearance
| Token | Used for |
|---|---|
--se-accent | color |
--se-border | border-block-end |
--se-border-width | border-block-end |
--se-control-h | min-block-size |
--se-duration-fast | transition |
--se-duration-instant | transition |
--se-ease-standard | transition |
--se-focus-color | outline |
--se-focus-width | outline, outline-offset |
--se-icon-sm | block-size, inline-size |
--se-leading-body | line-height |
--se-radius-sm | border-radius |
--se-space-3 | gap, padding-block |
--se-space-4 | padding-block-end |
--se-text | color |
--se-text-muted | color |
--se-text-sm | font-size |
Motion
Height animates over --se-duration-fast when the React binding is used. Without it the region snaps, which is correct rather than degraded — a snap is the platform's answer and it is instant.
Content
The summary says what is inside, closed. Advanced, Shipping details. A summary that reads Click to expand has spent the one line the reader gets on nothing.
Usage
Reach for the React binding when you need controlled state or an animated open; otherwise <details> is less machinery for the same result.
One section, and it knows about nothing else. A disclosure has no opinion about any other disclosure on the page: no shared heading level, no rule about how many may be open, no group relationship. That is the whole difference from Accordion, and it is why three of these stacked are not an accordion — a screen-reader user hears three unrelated toggles instead of one set they can skim by heading.
When not to use it. For a set of sections at one heading level, use Accordion. If the sections are alternatives and only one is ever relevant, use Tabs.
Notes
This is one of two components that look like they need a binding and do not — the other is RadioGroup. A component moves up a tier by taking a native element, never by adding script to the core.