Skip to content

Disclosure

Overview

The shared expanded/collapsed primitive.

Pattern: Disclosure. Status: stable.

When to use

  • A single expandable region with a trigger and panel relationship.
  • Controlled or uncontrolled sections that do not need group-level roving focus.

When not to use

  • Multiple coordinated sections; use Accordion.
  • State that should be represented by native details/summary without extra behavior.

Import

ts
import { createDisclosure } from 'ui-headless-runtime';

Controller creation

Create Disclosure during component mount or setup, subscribe before rendering derived UI, and keep every cleanup returned by registrations or DOM binding.

Options

  • Public options: disabled, id, defaultValue, getValue, onValueChange, subscribeValue.
  • getValue, onValueChange, and subscribeValue enable controlled expanded state; there are no open, defaultOpen, or onOpenChange options.

Snapshot

  • Snapshot fields: controlled, disabled, expanded, panel, trigger.
  • trigger contains id, ariaControls, ariaExpanded, and disabled; panel contains id, ariaLabelledby, hidden, and role.

Commands

  • Component commands: collapse, expand, handleTriggerClick, handleTriggerKeyDown, setDisabled, toggle.
  • Disclosure exposes metadata rather than a DOM bind method. handleTriggerKeyDown is for non-native triggers; native buttons normally forward click only.

Events

  • Events: beforeClose, beforeOpen, close, open, afterClose, afterOpen, stateChange.
  • Before-events may cancel a transition. Disabled or duplicate requests are no-ops and do not emit a separate ignored event.

Change reasons

  • Change reasons: programmatic, trigger, keyboard.

Controlled mode

Controlled Disclosure emits requested values and mirrors the consumer-provided open value.

Uncontrolled mode

Uncontrolled Disclosure owns the boolean state and is the base primitive for Collapsible.

DOM binding

  • Bind trigger and panel if you want runtime click/keyboard helpers, or apply snapshot metadata manually.

Required markup

  • Use a button trigger and a region/panel connected by aria-controls.

ARIA contract

  • Apply aria-expanded, aria-controls, and panel labelling from the snapshot.

Keyboard interaction

  • Enter / Space: Toggle expansion from the trigger.

Focus behavior

  • Enter and Space toggle from the trigger; Tab remains normal document navigation.

Nested behavior

  • Nested disclosures are independent unless a parent component such as Accordion coordinates them.

Cleanup

  • Release the binding and subscriptions before removing trigger or panel elements.

Minimal lifecycle example

ts
import { createDisclosure } from 'ui-headless-runtime';

const controller = createDisclosure();
const unsubscribe = controller.subscribe((snapshot) => {
  console.log(snapshot);
});

console.log(controller.getSnapshot());
unsubscribe();
controller.destroy();

The production demo loads the exact executable module from apps/demo/src/examples/disclosure.ts.

Edge cases

  • uncontrolled: The controller owns expansion.
  • controlled: An external store owns expansion.
  • disabled: Interaction becomes a no-op.

Limitations

  • Animation, height measurement, and visual persistence are intentionally outside the runtime.

API reference

See createDisclosure.

Headless behavior; consumer-owned rendering and styling.