Skip to content
Tungsten
Components/Disclosure·

Disclosure

since v4.3.0

Headless 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/tungsten
pnpm add @hey-mike/tungsten
import { 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 />.

PropTypeDefaultDescription
openbooleanControlled 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.
defaultOpenbooleanInitial open state in uncontrolled mode. Ignored when open is provided.

Sub-components

Composition slots re-exported from the same module.

DisclosureTrigger

PropTypeDefaultDescription
asChildbooleanWhen true, merge props onto the single React child instead of rendering a <button>.

DisclosurePanel

PropTypeDefaultDescription
asChildbooleanWhen 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).
animatebooleanOpt 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

Source