Skip to content

cv-accordion

Vertically stacked set of interactive sections that expand or collapse to reveal content.

Headless: createAccordion

Usage

Use cv-accordion for a true grouped composite: related sections with controlled expansion state, roving focus, heading semantics, multiple expanded values, or a rule that at least one section stays open. Use cv-disclosure or native <details>/<summary> for independent one-off reveals.

View source
html
<div class="accordion-demo-shell" data-demo="accordion" data-live-demo-height="760">
  <section class="accordion-demo-hero" aria-labelledby="accordion-demo-title">
    <div class="accordion-demo-copy">
      <span class="accordion-demo-kicker">Composite disclosure</span>
      <h3 id="accordion-demo-title">Grouped sections with roving focus and controlled expansion.</h3>
      <p>
        Use it when a surface needs one source of truth for expanded values, keyboard movement between
        headers, or an invariant such as one section always staying open.
      </p>
    </div>

    <dl class="accordion-demo-metrics">
      <div>
        <dt>Mode</dt>
        <dd>single or multiple</dd>
      </div>
      <div>
        <dt>Focus</dt>
        <dd>Arrow keys, Home, End</dd>
      </div>
      <div>
        <dt>Event payload</dt>
        <dd>{value, values, activeId}</dd>
      </div>
    </dl>
  </section>

  <section class="accordion-demo-board" aria-label="Accordion usage examples">
    <div class="accordion-demo-panel accordion-demo-panel--primary">
      <div class="accordion-demo-section-header">
        <span class="accordion-demo-label">Single controlled value</span>
        <h4>Credential record audit trail</h4>
      </div>

      <cv-accordion
        id="credential-accordion"
        class="accordion-demo-primary-accordion"
        value="identity"
        aria-label="Credential record sections"
      >
        <cv-accordion-item value="identity">
          <span slot="trigger">Identity and target</span>
          <div class="accordion-demo-content">
            <p>Visible record fields stay compact while the current section owns the review context.</p>
            <ul>
              <li>Login URL and account handle</li>
              <li>Last verified workspace</li>
              <li>Visible alias for shared records</li>
            </ul>
          </div>
        </cv-accordion-item>

        <cv-accordion-item value="rotation">
          <span slot="trigger">Rotation policy</span>
          <div class="accordion-demo-content">
            <p>The panel can host forms, status rows, or explanatory content without leaving the group.</p>
            <dl class="accordion-demo-proof-list">
              <div>
                <dt>Next check</dt>
                <dd>14 days</dd>
              </div>
              <div>
                <dt>Signal</dt>
                <dd>breach monitor clean</dd>
              </div>
            </dl>
          </div>
        </cv-accordion-item>

        <cv-accordion-item value="recovery" disabled>
          <span slot="trigger">Recovery material locked</span>
          <p>Disabled items stay in the order but cannot be toggled or selected.</p>
        </cv-accordion-item>
      </cv-accordion>
    </div>

    <aside class="accordion-demo-side" aria-label="Multiple and required-open examples">
      <div class="accordion-demo-panel">
        <div class="accordion-demo-section-header">
          <span class="accordion-demo-label">Multiple values</span>
          <h4>Security checks can stay open together.</h4>
        </div>

        <cv-accordion id="security-accordion" allow-multiple aria-label="Security checks">
          <cv-accordion-item value="password">
            <span slot="trigger">Password policy</span>
            <p>Length, rotation, and breach-monitoring requirements.</p>
          </cv-accordion-item>
          <cv-accordion-item value="devices">
            <span slot="trigger">Trusted devices</span>
            <p>Devices that can unlock this workspace.</p>
          </cv-accordion-item>
        </cv-accordion>
      </div>

      <div class="accordion-demo-panel accordion-demo-event-panel">
        <div class="accordion-demo-section-header">
          <span class="accordion-demo-label">Event contract</span>
          <h4>Read interaction state from events, not the DOM.</h4>
        </div>

        <output id="accordion-event-output" class="accordion-demo-event-output" aria-live="polite">
          value: identity | values: identity | activeId: identity
        </output>
      </div>

      <div class="accordion-demo-panel">
        <div class="accordion-demo-section-header">
          <span class="accordion-demo-label">Required open</span>
          <h4>At least one setup step remains expanded.</h4>
        </div>

        <cv-accordion id="required-accordion" value="intro" aria-label="Required setup">
          <cv-accordion-item value="intro">
            <span slot="trigger">Threat model note</span>
            <p>Read the visible-state assumptions before changing the setup.</p>
          </cv-accordion-item>
          <cv-accordion-item value="details">
            <span slot="trigger">Operational details</span>
            <p>Configuration rules and review notes stay available.</p>
          </cv-accordion-item>
        </cv-accordion>
      </div>
    </aside>
  </section>
</div>

<script type="module">
  const securityAccordion = document.querySelector('#security-accordion')
  const requiredAccordion = document.querySelector('#required-accordion')
  const credentialAccordion = document.querySelector('#credential-accordion')
  const eventOutput = document.querySelector('#accordion-event-output')

  securityAccordion.expandedValues = ['password', 'devices']
  requiredAccordion.allowZeroExpanded = false

  credentialAccordion.addEventListener('cv-input', (event) => {
    const {value, values, activeId} = event.detail
    eventOutput.value = `value: ${value ?? 'none'} | values: ${values.join(', ') || 'none'} | activeId: ${
      activeId ?? 'none'
    }`
  })
</script>
html
<!-- Controlled single mode (default) -->
<cv-accordion value="identity" aria-label="Credential sections">
  <cv-accordion-item value="identity">
    <span slot="trigger">Identity</span>
    <p>Username, display name, and login URL.</p>
  </cv-accordion-item>
  <cv-accordion-item value="recovery">
    <span slot="trigger">Recovery</span>
    <p>Backup codes and recovery contact notes.</p>
  </cv-accordion-item>
</cv-accordion>

<!-- Multiple mode with a controlled expandedValues property -->
<cv-accordion id="security-accordion" allow-multiple aria-label="Security checks">
  <cv-accordion-item value="password">
    <span slot="trigger">Password policy</span>
    <p>Length, rotation, and breach-monitoring requirements.</p>
  </cv-accordion-item>
  <cv-accordion-item value="devices">
    <span slot="trigger">Trusted devices</span>
    <p>Devices that can unlock this workspace.</p>
  </cv-accordion-item>
</cv-accordion>

<script type="module">
  document.querySelector('#security-accordion').expandedValues = ['password', 'devices']
</script>

<!-- One section must stay open.
     In plain HTML, set allowZeroExpanded as a JS property because
     boolean attributes cannot represent false. -->
<cv-accordion id="required-accordion" value="intro" aria-label="Required setup">
  <cv-accordion-item value="intro">
    <span slot="trigger">Introduction</span>
    <p>Read this before changing the setup.</p>
  </cv-accordion-item>
  <cv-accordion-item value="details">
    <span slot="trigger">Details</span>
    <p>Configuration rules and operational notes.</p>
  </cv-accordion-item>
</cv-accordion>

<script type="module">
  document.querySelector('#required-accordion').allowZeroExpanded = false
</script>

<!-- Custom heading level -->
<cv-accordion heading-level="4">
  <cv-accordion-item value="s1">
    <span slot="trigger">Under an h3</span>
    <p>Content here.</p>
  </cv-accordion-item>
</cv-accordion>

<!-- Custom icons -->
<cv-accordion>
  <cv-accordion-item value="custom">
    <span slot="trigger">Custom icons</span>
    <span slot="expand-icon">
      <svg
        viewBox="0 0 24 24"
        aria-hidden="true"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
        <path d="M5 12h14"></path>
        <path d="M12 5v14"></path>
      </svg>
    </span>
    <span slot="collapse-icon">
      <svg
        viewBox="0 0 24 24"
        aria-hidden="true"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
        <path d="M5 12h14"></path>
      </svg>
    </span>
    <p>Content with plus/minus icons.</p>
  </cv-accordion-item>
</cv-accordion>

<!-- Disabled item -->
<cv-accordion>
  <cv-accordion-item value="enabled">
    <span slot="trigger">Enabled</span>
    <p>This section works.</p>
  </cv-accordion-item>
  <cv-accordion-item value="locked" disabled>
    <span slot="trigger">Locked</span>
    <p>This section cannot be toggled.</p>
  </cv-accordion-item>
</cv-accordion>

Anatomy

<cv-accordion> (host)
└── <section part="base" aria-label="…">
    └── <slot>                                     ← cv-accordion-item elements

Attributes

AttributeTypeDefaultDescription
valueString""Expanded section value (single mode). Reflects the first expanded item's value.
allow-multipleBooleanfalseAllow multiple sections expanded simultaneously
allow-zero-expandedBooleantrueAllow all sections to be collapsed. When false, at least one section must remain expanded.
heading-levelNumber3Heading level (1–6) for all item headers
aria-labelString""Accessible label for the accordion group

JS-only property:

PropertyTypeDefaultDescription
expandedValuesstring[][]Array of expanded section values (meaningful in allow-multiple mode)

Slots

SlotDescription
(default)cv-accordion-item elements

CSS Parts

PartElementDescription
base<section>Root wrapper with aria-label

CSS Custom Properties

PropertyDefaultDescription
--cv-accordion-gapvar(--cv-space-2, 8px)Spacing between accordion items

Visual States

Host selectorDescription
:host([allow-multiple])Multiple sections can be expanded

Events

EventDetailDescription
cv-input{value, values, activeId}Fires on any interaction (expand/collapse or focus change)
cv-change{value, values, activeId}Fires only when expanded sections change

Event detail shape:

FieldTypeDescription
valuestring | nullFirst expanded item value, or null if none
valuesstring[]All expanded item values
activeIdstring | nullCurrently focused item value

Child Elements

cv-accordion-item

Anatomy

<cv-accordion-item> (host)
└── <div part="base">
    ├── <h3 part="header" id="…">          ← heading level from parent
    │   └── <button part="trigger" aria-expanded="…" aria-controls="…">
    │       ├── <slot name="trigger">       ← header label
    │       └── <span part="indicator trigger-icon" aria-hidden="true">
    │           ├── <slot name="expand-icon">css chevron</slot>   ← shown when collapsed
    │           └── <slot name="collapse-icon">css chevron</slot>  ← shown when expanded
    └── <div part="panel" role="region" aria-labelledby="…" data-state="…">
        └── <div part="panel-content">
            └── <slot>                      ← panel content

Attributes

AttributeTypeDefaultDescription
valueString""Unique identifier for this section. Auto-generated as section-{n} if empty.
disabledBooleanfalsePrevents toggling this section
expandedBooleanfalseWhether panel content is visible (reflected, managed by parent)
activeBooleanfalseWhether this item's trigger has roving focus (reflected, managed by parent)

Slots

SlotDescription
(default)Panel content
triggerHeader label text
expand-iconIcon shown when the panel is collapsed. Default: CSS-drawn chevron
collapse-iconIcon shown when the panel is expanded. Default: CSS-drawn chevron

CSS Parts

PartElementDescription
base<div>Cohesive item surface
header<h3>Heading element (level controlled by parent's heading-level)
trigger<button>Interactive toggle button
indicator<span>Wrapper around expand/collapse icon slots
trigger-icon<span>Compatibility alias on the same element as indicator
panel<div>Expandable content region; data-state is closed, opening, open, or closing
panel-content<div>Inner panel content wrapper used to animate height without clipping the outer item surface

CSS Custom Properties

PropertyDefaultDescription
--cv-accordion-item-trigger-min-height44pxMinimum height of the trigger button
--cv-accordion-item-trigger-padding-inlinevar(--cv-space-4, 16px)Horizontal padding of trigger
--cv-accordion-item-trigger-border-radiusvar(--cv-radius-md, 10px)Border radius of trigger
--cv-accordion-item-trigger-gapvar(--cv-space-3, 12px)Gap between trigger label and icon
--cv-accordion-item-panel-paddingvar(--cv-space-4, 16px)Padding inside the panel content wrapper
--cv-accordion-item-panel-border-radiusvar(--cv-radius-md, 10px)Border radius of the item surface
--cv-accordion-item-gapvar(--cv-space-0, 0px)Gap between trigger and panel
--cv-accordion-item-indicator-size22pxSize of the indicator icon area
--cv-accordion-item-durationvar(--cv-duration-medium, 250ms)Transition duration for panel and state glow
--cv-accordion-item-easingvar(--cv-easing-decelerate, ease)Easing function for panel and state motion

Visual States

Host selectorDescription
:host([expanded])Panel visible; collapse-icon shown, expand-icon hidden; active border signal
:host([active])Trigger has roving focus; item border and indicator receive primary signal
:host([disabled])Trigger opacity reduced (0.55), cursor: not-allowed, interaction blocked

Panel Motion

expanded reflects the headless accordion state immediately. The item keeps the panel mounted only for the visual close transition:

  • data-state="open": panel is visible and interactive.
  • data-state="opening": panel is mounted for the next animation frame before transitioning to open.
  • data-state="closing": panel remains mounted, has aria-hidden="true" and inert, and transitions to the collapsed visual state.
  • data-state="closed": panel is hidden.

When prefers-reduced-motion: reduce matches, close transitions complete immediately.

ChromVoid UIKit documentation