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>Lightweight CSS chevron 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