Disclosure
since v4.3.0Headless single-section disclosure primitive. Owns open/closed state and
a11y wiring (aria-expanded, aria-controls, hidden); the consumer
styles the trigger and panel however they like. The panel is always mounted
(just hidden when closed) so form state inside survives toggle cycles.
For multi-section collapsible groups, use {@link Accordion } instead.
Install
Add the package and import the component.
pnpm add @hey-mike/tungstenpnpm add @hey-mike/tungstenimport { Disclosure } from '@hey-mike/tungsten';import { Disclosure } from '@hey-mike/tungsten';Preview
Same fixtures used by the visual-regression suite.
Usage
apps/docs/app/snapshots/disclosure/page.tsx
import { Disclosure, DisclosureTrigger, DisclosurePanel } from '@hey-mike/tungsten';
import { VariantGrid } from '../_components/VariantGrid';
function Closed() {
return (
<Disclosure>
<DisclosureTrigger>What is Tungsten?</DisclosureTrigger>
<DisclosurePanel>A component library for LuoForge products.</DisclosurePanel>
</Disclosure>
);
}
function Open() {
return (
<Disclosure defaultOpen>
<DisclosureTrigger>What is Tungsten?</DisclosureTrigger>
<DisclosurePanel>A component library for LuoForge products.</DisclosurePanel>
</Disclosure>
);
}
function MultipleItems() {
return (
<div className="flex w-64 flex-col gap-2">
<Disclosure defaultOpen>
<DisclosureTrigger>First item (open)</DisclosureTrigger>
<DisclosurePanel>Content for the first item.</DisclosurePanel>
</Disclosure>
<Disclosure>
<DisclosureTrigger>Second item (closed)</DisclosureTrigger>
<DisclosurePanel>Content for the second item.</DisclosurePanel>
</Disclosure>
<Disclosure>
<DisclosureTrigger>Third item (closed)</DisclosureTrigger>
<DisclosurePanel>Content for the third item.</DisclosurePanel>
</Disclosure>
</div>
);
}
export default function DisclosureSnapshot() {
return (
<VariantGrid
title="Disclosure"
variants={[
{ label: 'closed', node: <Closed /> },
{ label: 'open', node: <Open /> },
{ label: 'multiple', node: <MultipleItems /> },
]}
/>
);
}
import { Disclosure, DisclosureTrigger, DisclosurePanel } from '@hey-mike/tungsten';
import { VariantGrid } from '../_components/VariantGrid';
function Closed() {
return (
<Disclosure>
<DisclosureTrigger>What is Tungsten?</DisclosureTrigger>
<DisclosurePanel>A component library for LuoForge products.</DisclosurePanel>
</Disclosure>
);
}
function Open() {
return (
<Disclosure defaultOpen>
<DisclosureTrigger>What is Tungsten?</DisclosureTrigger>
<DisclosurePanel>A component library for LuoForge products.</DisclosurePanel>
</Disclosure>
);
}
function MultipleItems() {
return (
<div className="flex w-64 flex-col gap-2">
<Disclosure defaultOpen>
<DisclosureTrigger>First item (open)</DisclosureTrigger>
<DisclosurePanel>Content for the first item.</DisclosurePanel>
</Disclosure>
<Disclosure>
<DisclosureTrigger>Second item (closed)</DisclosureTrigger>
<DisclosurePanel>Content for the second item.</DisclosurePanel>
</Disclosure>
<Disclosure>
<DisclosureTrigger>Third item (closed)</DisclosureTrigger>
<DisclosurePanel>Content for the third item.</DisclosurePanel>
</Disclosure>
</div>
);
}
export default function DisclosureSnapshot() {
return (
<VariantGrid
title="Disclosure"
variants={[
{ label: 'closed', node: <Closed /> },
{ label: 'open', node: <Open /> },
{ label: 'multiple', node: <MultipleItems /> },
]}
/>
);
}
Props
Surface specific to <Disclosure />.
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | — | Controlled open state. |
| onOpenChange | ((open: boolean) => void) | ((open: boolean) => void) | — | Fires with the next open state. Required alongside open.
Optional change callback in uncontrolled mode (e.g. analytics). State is still owned internally. |
| defaultOpen | boolean | — | Initial open state in uncontrolled mode. Ignored when open is provided. |
Sub-components
Composition slots re-exported from the same module.
DisclosureTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | — | When true, merge props onto the single React child instead of rendering a <button>. |
DisclosurePanel
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | — | When true, merge id/hidden onto the single React child instead of
rendering the styled default <div>. Use this when the panel's default
classes (pt-3 pb-4 text-sm text-ink-2) must not apply: cn is
clsx-only, so a conflicting consumer class loses or wins by stylesheet
order, not argument order — composition is the supported escape hatch
(same contract as {@link DisclosureTrigger}'s asChild). |
| animate | boolean | — | Opt into an animated height-collapse (framer-motion) instead of the
default instant hidden snap. The collapsed panel is height: 0 +
inert + aria-hidden (the hidden attribute is NOT used); content
stays mounted so form state survives, and prefers-reduced-motion
collapses the transition to an instant snap. Only overflow-hidden +
the consumer className are applied — the styled-div defaults do not.
Mutually exclusive with {@link DisclosurePanelProps.asChild}: animate
needs the motion.div wrapper, so passing both dev-warns and the animate
path wins (asChild is ignored).
@defaultValue false |