useDisclosure
A headless show / hide primitive for disclosure widgets — expandable panels, accordions, menus, the collapsible sidebar groups on this very site. It owns the open state, the WAI-ARIA wiring, and the toggle handler; you own the markup and the styles.
This is the Vue binding of the headless layer; the framework-agnostic contract lives in
@oriui/headless. Behavior is swappable per primitive through the OriHeadless adapter,
and a zero-dependency native engine ships as the default — so it works with no setup.
Import
import { useDisclosure } from '@oriui/headless/vue'
Options
Pass an options object — or a ref / getter returning one. disabled is live: it is re-read whenever
the option changes, so you can bind it to state. id and defaultOpen are seeds — they are read once,
when the primitive is created, and later changes to them are ignored:
| Option | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | false | Initial open state (uncontrolled). |
disabled | boolean | false | Ignore toggles while true. |
id | string | auto | Base id for the trigger / content ARIA wiring. |
Returns
useDisclosure returns a DisclosureControl — reactive state, ARIA-correct prop bags to spread onto
your markup, and imperative handlers.
| Property | Type | Description |
|---|---|---|
open | ComputedRef<boolean> | Current open state. |
rootProps | ComputedRef<object> | Bind to the root wrapper (data-state). |
triggerProps | ComputedRef<object> | Bind to the toggle control (aria-expanded, aria-controls, click, disabled). |
contentProps | ComputedRef<object> | Bind to the collapsible content (id, role, aria-labelledby, hidden). |
setOpen(open) | (open: boolean) => void | Imperatively set the state. |
toggle() | () => void | Flip the state. |
Usage
Spread the prop bags onto your own elements — the primitive supplies the behavior, you supply the look:
<script setup lang="ts">
import { useDisclosure } from '@oriui/headless/vue'
const { open, triggerProps, contentProps } = useDisclosure(() => ({ defaultOpen: false }))
</script>
<template>
<button v-bind="triggerProps">Details {{ open ? '▲' : '▼' }}</button>
<div v-bind="contentProps">…content…</div>
</template>
@oriui/cssis framework-free: the same.ori-*classes and tokens style this markup in any framework.
Adapter
The behavior resolves through the OriHeadless contract. With nothing wired, useDisclosure falls
back to the native @oriui/headless engine — zero dependencies, no setup. To swap in a different
engine (Zag, or your own), provide a disclosure adapter once at the app root:
import { OriHeadless } from '@oriui/headless/vue'
import { myDisclosureAdapter } from './headless/my-disclosure'
app.use(OriHeadless, { disclosure: myDisclosureAdapter })
The adapter must satisfy the DisclosureAdapter contract — same DisclosureControl shape — so your
markup never changes when you swap engines. To scope an adapter to one subtree instead of the whole
app, call provideHeadless({ disclosure: … }) inside that subtree's setup.
Accessibility
The prop bags carry the full WAI-ARIA Disclosure pattern, so the wiring is correct as long as you bind them:
triggerPropsrenders a real<button type="button">witharia-expandedreflecting the open state andaria-controlspointing at the content. Whendisabled, it sets the realdisabledattribute (plusdata-disabledfor styling).contentPropssetsrole="region",aria-labelledbyback to the trigger (so the region takes its accessible name from the toggle), and a realhiddenattribute while collapsed — the content is removed from the accessibility tree and tab order when closed.- Clicking the trigger toggles the state; activation is the native button behavior, so
EnterandSpaceboth work with no extra handlers. rootProps,triggerProps, andcontentPropseach expose adata-stateofopen/closedas a styling hook for transitions.
| Key | Action |
|---|---|
Enter | Toggles the disclosure (native button). |
Space | Toggles the disclosure (native button). |
See also
- @oriui/headless — the framework-agnostic contract and the native engine behind this binding.
- useDialog — the modal counterpart; the hard-behavior case delegated to an adapter.
- CSS layer — standalone
.ori-*classes for styling the markup you bind these props to.