Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Accordion

Disclosure groups on the native `<details>` element.

Installation

pnpm dlx zazz-ui add accordion
npx zazz-ui add accordion
yarn dlx zazz-ui add accordion
bunx zazz-ui add accordion

Run zazz-ui init first if the project has no zazz/ folder yet.

Examples

What is an accordion?

A disclosure widget built from the native <details> and <summary> elements. The open/close animation uses the ::details-content pseudo-element — no JavaScript.

How does it work?

Each item is a separate <details>, so several can stay open at once. Click a summary or focus it and press Enter to toggle.

Why use it?

Keyboard support, state, and accessibility come from the platform — Zazz only styles the chrome through theme tokens.

<ui-accordion>
  <details
    style="
      --py: 4;
      --border-b: 1;
    "
  >
    <summary
      style="
        --font-weight: strong;
        --text: currentColor;
        --text--hover: var(--color-muted-foreground);
      "
    >
      What is an accordion?
      <svg data-accordion-slot="marker" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
        <rect width="256" height="256" fill="none" />
        <polyline
          points="208 96 128 176 48 96"
          fill="none"
          stroke="currentColor"
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="16"
        />
      </svg>
    </summary>
    <div style="--pb: 4">
      <p>
        A disclosure widget built from the native <code>&lt;details&gt;</code> and
        <code>&lt;summary&gt;</code> elements. The open/close animation uses the
        <code>::details-content</code> pseudo-element — no JavaScript.
      </p>
    </div>
  </details>
  <details
    style="
      --py: 4;
      --border-b: 1;
    "
  >
    <summary
      style="
        --font-weight: strong;
        --text: currentColor;
        --text--hover: var(--color-muted-foreground);
      "
    >
      How does it work?
      <svg data-accordion-slot="marker" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
        <rect width="256" height="256" fill="none" />
        <polyline
          points="208 96 128 176 48 96"
          fill="none"
          stroke="currentColor"
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="16"
        />
      </svg>
    </summary>
    <div style="--pb: 4">
      <p>
        Each item is a separate <code>&lt;details&gt;</code>, so several can stay open at once.
        Click a summary or focus it and press Enter to toggle.
      </p>
    </div>
  </details>
  <details
    style="
      --py: 4;
      --border-b: 1;
    "
  >
    <summary
      style="
        --font-weight: strong;
        --text: currentColor;
        --text--hover: var(--color-muted-foreground);
      "
    >
      Why use it?
      <svg data-accordion-slot="marker" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
        <rect width="256" height="256" fill="none" />
        <polyline
          points="208 96 128 176 48 96"
          fill="none"
          stroke="currentColor"
          stroke-linecap="round"
          stroke-linejoin="round"
          stroke-width="16"
        />
      </svg>
    </summary>
    <div style="--pb: 4">
      <p>
        Keyboard support, state, and accessibility come from the platform — Zazz only styles the
        chrome through theme tokens.
      </p>
    </div>
  </details>
</ui-accordion>
/**
 * accordion.css: Accordion (ui-accordion | [data-ui~="accordion"] on <details>/<summary>;
 *                slot data-accordion-slot="marker" on the summary icon that rotates open)
 *
 * @layer      variables, reset, components
 * @requires   _layers.css, _variables.css, _properties.css (--_focus-shadow)
 * @uses       ::details-content: native open/close block-size transition (reset layer)
 * @uses       interpolate-size: allow-keywords (from _variables.css; required for
 *             block-size: 0 -> auto transitions)
 * @tokens     --ui-accordion-* (@layer variables; none backs a utility)
 */

@layer variables {
  :root {
    --ui-accordion-icon-size: calc(var(--spacing) * 4);
    --ui-accordion-icon-transform--open: rotate(-180deg);
  }
}

@layer reset {
  /* ===========================================================================
  DETAILS & SUMMARY: native open/close transition + summary marker reset
  =========================================================================== */

  :where(details)::details-content {
    --details-content-transition:
      opacity var(--default-transition-duration) var(--default-transition-timing-function),
      transform var(--default-transition-duration) var(--default-transition-timing-function),
      block-size var(--default-transition-duration) var(--default-transition-timing-function),
      content-visibility var(--default-transition-duration)
        var(--default-transition-timing-function) allow-discrete;

    block-size: 0;
    overflow: hidden;
    transition: var(--details-content-transition);
    will-change: opacity, transform, block-size, content-visibility;
  }

  :where(details[open])::details-content {
    block-size: auto;
  }

  :where(summary) {
    display: flex;
    text-decoration: none;
    user-select: none;
    list-style: none;
  }
}

@layer ui.components {
  :where(ui-accordion, [data-ui~="accordion"]) {
    display: grid;
    inline-size: 100%;
    flex: 1;
  }

  :where(ui-accordion, [data-ui~="accordion"]) > details {
    display: grid;
    flex: 1 0 auto;
  }

  :where(ui-accordion, [data-ui~="accordion"]) > details > summary {
    display: flex;
    align-items: center;
    justify-content: space-between;

    /* The ring is published to --_focus-shadow so a --shadow utility on the
       summary keeps it; the ring widths grow from 0 so the ring eases in. */
    --_ring-offset-width: 0px;
    --_ring-width: 0px;
    --_focus-in: max(0, -1 * sign(var(--_focus-ring-w, 1px)));
    --_focus-shadow:
      0 0 0 calc(var(--_ring-offset-width) * (1 - var(--_focus-in))) var(--ring-offset-shadow-color),
      0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * (1 - var(--_focus-in)))
        var(--_ring-fill, var(--ring-shadow-color)),
      inset 0 0 0 calc(var(--_ring-offset-width) * var(--_focus-in)) var(--ring-offset-shadow-color),
      inset 0 0 0 calc((var(--_ring-offset-width) + var(--_ring-width)) * var(--_focus-in))
        var(--_ring-fill, var(--ring-shadow-color));
    box-shadow: var(--_focus-shadow, 0 0 #0000), var(--_shadow-resolved, 0 0 #0000);

    /* transparent twin of the ring for forced-colors / high-contrast modes */
    outline: abs(var(--_focus-ring-w, var(--outline-width))) var(--outline-style) transparent;
    outline-offset: calc(
      var(--outline-offset) - var(--_focus-in) *
        (2 * var(--outline-offset) + abs(var(--_focus-ring-w, var(--outline-width))))
    );

    transition: var(--default-transition);
  }

  :where(ui-accordion, [data-ui~="accordion"]) > details > summary:focus-visible {
    --_ring-offset-width: var(--ring-offset-width);
    --_ring-width: abs(var(--_focus-ring-w, var(--ring-width)));

    outline-color: transparent;
  }

  :where(ui-accordion, [data-ui~="accordion"]) summary > [data-accordion-slot="marker"] {
    display: flex;
    align-items: center;
    justify-content: center;
    inline-size: var(--ui-accordion-icon-size);
    block-size: var(--ui-accordion-icon-size);
    pointer-events: none;
    transition: transform var(--default-transition-duration)
      var(--default-transition-timing-function);
  }

  :where(ui-accordion, [data-ui~="accordion"])
    > details[open]
    > summary
    > [data-accordion-slot="marker"] {
    transform: var(--ui-accordion-icon-transform--open);
  }
}

Attributes

accordion attributes
AttributeKindValuesNotes
data-ui="accordion"Identityfree valueIdentity token in data-ui.
<ui-accordion>Tag formfree valueTag form; styled and, when scripted, registered under this name.
data-accordion-slotSlotmarkerSlot: a part inside the primitive.

Hooks

Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.

accordion hooks
HookDefaultState
--ui-accordion-icon-sizecalc(var(--spacing) * 4)—
--ui-accordion-icon-transform--openrotate(-180deg)open