Skip to content

cv-disclosure

Expandable panel that controls the visibility of a content area, with support for exclusive accordion-like grouping via a shared name.

Headless: createDisclosure

Usage

Use cv-disclosure for one local reveal: advanced settings, optional help, or a single FAQ answer. Prefer native <details>/<summary> for static content that does not need ChromVoid styling, events, or headless state. Use cv-accordion instead when the UI is a coordinated set of sections with controlled value(s), roving focus, heading levels, or "at least one open" rules.

View source
html
<div class="disclosure-demo-shell" data-demo="disclosure" data-live-demo-height="820">
  <section class="disclosure-demo-hero" aria-labelledby="disclosure-demo-title">
    <div class="disclosure-demo-copy">
      <span class="disclosure-demo-kicker">Local reveal primitive</span>
      <h3 id="disclosure-demo-title">
        Expose optional detail without turning the surface into an accordion.
      </h3>
      <p>
        The component keeps click, keyboard, disabled, and named-group behavior in the headless disclosure
        model. UIKit owns the visual trigger and panel only.
      </p>
    </div>

    <dl class="disclosure-demo-metrics" aria-label="Disclosure contract summary">
      <div>
        <dt>State</dt>
        <dd>open / closed / disabled</dd>
      </div>
      <div>
        <dt>Group</dt>
        <dd>optional exclusive name</dd>
      </div>
      <div>
        <dt>Events</dt>
        <dd>cv-input + cv-change</dd>
      </div>
    </dl>
  </section>

  <section class="disclosure-demo-workbench" aria-label="Disclosure examples in a vault review">
    <div class="disclosure-demo-panel" aria-labelledby="disclosure-demo-panel-title">
      <div class="disclosure-demo-section-header">
        <span class="disclosure-demo-kicker">Visible profile review</span>
        <h4 id="disclosure-demo-panel-title">
          Use one-off reveals for evidence, limits, and advanced notes.
        </h4>
      </div>

      <div class="disclosure-demo-stack">
        <cv-disclosure open data-disclosure-label="Routing proof">
          <span slot="trigger" class="disclosure-demo-trigger">
            <strong>Routing proof</strong>
            <small>Initially open state</small>
          </span>
          <div class="disclosure-demo-content">
            <p>
              The visible vault route resolves through the active device boundary. Hidden namespaces are not
              listed in this disclosure because they are outside the current threat model.
            </p>
            <dl class="disclosure-demo-proof">
              <div>
                <dt>Route</dt>
                <dd>travel-profile.visible</dd>
              </div>
              <div>
                <dt>Boundary</dt>
                <dd>paired hardware</dd>
              </div>
            </dl>
          </div>
        </cv-disclosure>

        <cv-disclosure data-disclosure-label="Advanced relay options">
          <span slot="trigger" class="disclosure-demo-trigger">
            <strong>Advanced relay options</strong>
            <small>Single optional reveal</small>
          </span>
          <div class="disclosure-demo-content">
            <p>
              Tune retry windows and export notes only when the operator needs relay-level detail. The closed
              state keeps the routine review compact.
            </p>
          </div>
        </cv-disclosure>

        <cv-disclosure disabled data-disclosure-label="Locked recovery note">
          <span slot="trigger" class="disclosure-demo-trigger">
            <strong>Locked recovery note</strong>
            <small>Disabled state remains visible</small>
          </span>
          <div class="disclosure-demo-content">
            <p>This panel cannot be toggled while the workspace is in a protected review mode.</p>
          </div>
        </cv-disclosure>
      </div>
    </div>

    <aside class="disclosure-demo-side" aria-label="Grouped disclosure examples and event output">
      <div class="disclosure-demo-group">
        <div class="disclosure-demo-section-header">
          <span class="disclosure-demo-kicker">Named group</span>
          <h4>Opening one peer closes the others without roving focus or accordion value state.</h4>
        </div>

        <cv-disclosure name="disclosure-demo-security-notes" data-disclosure-label="Recovery phrase">
          <span slot="trigger" class="disclosure-demo-trigger">
            <strong>Recovery phrase</strong>
            <small>exclusive peer</small>
          </span>
          <p>Document storage location and operator access without exposing unrelated audit notes.</p>
        </cv-disclosure>

        <cv-disclosure name="disclosure-demo-security-notes" data-disclosure-label="Hardware token">
          <span slot="trigger" class="disclosure-demo-trigger">
            <strong>Hardware token</strong>
            <small>exclusive peer</small>
          </span>
          <p>Show enrollment date, fallback token label, and expected pairing state.</p>
        </cv-disclosure>

        <cv-disclosure name="disclosure-demo-security-notes" data-disclosure-label="Audit note">
          <span slot="trigger" class="disclosure-demo-trigger">
            <strong>Audit note</strong>
            <small>exclusive peer</small>
          </span>
          <p>Keep the visible trail short, specific, and tied to the selected vault surface.</p>
        </cv-disclosure>
      </div>

      <p class="disclosure-demo-log" role="status" aria-live="polite" data-disclosure-output>
        Waiting for user interaction. Toggle any active disclosure.
      </p>
    </aside>
  </section>
</div>

<script type="module">
  document
    .querySelectorAll('.disclosure-demo-shell[data-demo="disclosure"]:not([data-ready])')
    .forEach((shell) => {
      shell.dataset.ready = 'true'

      const output = shell.querySelector('[data-disclosure-output]')
      const disclosures = shell.querySelectorAll('cv-disclosure')

      disclosures.forEach((disclosure) => {
        disclosure.addEventListener('cv-change', (event) => {
          const detail = event.detail
          const label = disclosure.dataset.disclosureLabel ?? 'Disclosure'
          const state = detail.open ? 'opened' : 'closed'

          shell.dataset.lastState = detail.open ? 'open' : 'closed'
          if (output) output.textContent = `${label} ${state}.`
        })
      })
    })
</script>

Anatomy

<cv-disclosure> (host)
└── <div part="base">
    ├── <div part="trigger" role="button">
    │   ├── <slot name="trigger">
    │   └── <span part="trigger-icon" aria-hidden="true">
    └── <div part="panel">
        └── <slot>

Attributes

AttributeTypeDefaultDescription
openBooleanfalseWhether the panel content is visible
disabledBooleanfalsePrevents user interaction with the trigger
nameString""Group name for exclusive accordion-like behavior; when set, opening this disclosure closes all others sharing the same name

Slots

SlotDescription
(default)Panel content displayed when the disclosure is open
triggerLabel content rendered inside the trigger

CSS Parts

PartElementDescription
base<div>Root layout wrapper
trigger<div>Interactive trigger element with role="button"
trigger-icon<span>Chevron/arrow indicator that rotates when open
panel<div>Collapsible content container

CSS Custom Properties

PropertyDefaultDescription
--cv-disclosure-durationvar(--cv-duration-fast, 120ms)Duration of expand/collapse transitions
--cv-disclosure-easingvar(--cv-easing-standard, ease)Easing function for expand/collapse transitions

Additionally, component styles depend on theme tokens through fallback values:

Theme PropertyDefaultDescription
--cv-color-border#2a3245Border color for trigger and panel
--cv-color-surface#141923Trigger background color
--cv-color-surface-elevated#1d2432Panel background color
--cv-color-text#e8ecf6Default text color
--cv-color-text-muted#9aa6bfTrigger icon color
--cv-color-primary#65d7ffFocus outline color
--cv-radius-sm6pxBorder radius for trigger and panel
--cv-space-28pxGap between trigger and panel
--cv-space-312pxInline padding for trigger; block and inline padding for panel

Visual States

Host selectorDescription
:host([open])Panel visible; trigger icon rotated 90deg
:host([disabled])Trigger has opacity: 0.55, cursor: not-allowed; interaction blocked

Events

EventDetailDescription
cv-input{open: boolean}Fires immediately when user interaction changes the open state
cv-change{open: boolean}Fires when the open state commits after user interaction

Events fire only on user-initiated state changes (click, keyboard). Programmatic calls to show() / hide() or attribute changes do not fire events.

Imperative API

MethodReturnDescription
show()voidOpens the panel; delegates to headless actions.open()
hide()voidCloses the panel; delegates to headless actions.close()

Reactive State Mapping

cv-disclosure is a visual adapter over headless createDisclosure.

UIKit PropertyDirectionHeadless Binding
openattr -> actionactions.open() / actions.close() depending on value
disabledattr -> actionactions.setDisabled(value)
nameattr -> actionactions.setName(value)
Headless StateDirectionDOM Reflection
state.isOpen()state -> attr[open] host attribute
state.isDisabled()state -> attr[disabled] host attribute
state.name()state -> attr[name] host attribute
  • contracts.getTriggerProps() is spread onto the inner [part="trigger"] element to apply role, tabindex, aria-expanded, aria-controls, aria-disabled, and keyboard/click handlers.
  • contracts.getPanelProps() is spread onto the inner [part="panel"] element to apply id, aria-labelledby, and hidden.
  • show() and hide() delegate directly to actions.open() and actions.close() without firing cv-input/cv-change events.
  • actions.destroy() must be called in disconnectedCallback to unregister from the name group registry.
  • UIKit does not own toggle, keyboard, or grouping logic; headless state is the source of truth.

ChromVoid UIKit documentation