Navigation

Accordion

Stacked sections that open and close in place, for FAQs and settings too long to show at once.

Built on
<details><summary><details name>::details-contentinterpolate-size
Client JavaScript
None
Works without React
Yes, as HTML and CSS
Source, gzipped
1.4 kB
Dependencies
React
When am I charged?
On the 1st, for the month ahead.
Can I get a refund?
Within 14 days of any charge.
Try its options

Loading its options.

Code
import { Accordion, AccordionItem } from "@/components/equip/accordion";

export default function Demo() {
  return (
    <Accordion style={{ width: 216 }}>
      <AccordionItem name="billing" summary="When am I charged?" open>
        On the 1st, for the month ahead.
      </AccordionItem>
      <AccordionItem name="billing" summary="Can I get a refund?">
        Within 14 days of any charge.
      </AccordionItem>
    </Accordion>
  );
}
Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="width:216px" class="eq-accordion">
  <details name="billing" open class="eq-accordion-item">
    <summary class="eq-accordion-trigger">
      <span class="eq-accordion-title">When am I charged?</span>
      <svg class="eq-accordion-chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <path d="M4 6l4 4 4-4" />
      </svg>
    </summary>
    <div class="eq-accordion-content">On the 1st, for the month ahead.</div>
  </details>
  <details name="billing" class="eq-accordion-item">
    <summary class="eq-accordion-trigger">
      <span class="eq-accordion-title">Can I get a refund?</span>
      <svg class="eq-accordion-chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <path d="M4 6l4 4 4-4" />
      </svg>
    </summary>
    <div class="eq-accordion-content">Within 14 days of any charge.</div>
  </details>
</div>

Examples

One open at a time

Items that share a name form an exclusive group, so opening one closes the others with no JavaScript.

Why did my build time out?
Builds stop after 45 minutes. Cache node_modules between runs or split the job in two.
Where do environment variables live?
Under Settings, Environment. Values saved there apply from the next deploy.
Can I roll back a release?
Yes. Open Deployments, pick any earlier build and promote it to production.
Code
import { Accordion, AccordionItem } from "@/components/equip/accordion";

export default function Example() {
  return (
    <Accordion style={{ maxWidth: 420 }}>
      <AccordionItem name="deploy-faq" summary="Why did my build time out?" open>
        Builds stop after 45 minutes. Cache node_modules between runs or split the job in two.
      </AccordionItem>
      <AccordionItem name="deploy-faq" summary="Where do environment variables live?">
        Under Settings, Environment. Values saved there apply from the next deploy.
      </AccordionItem>
      <AccordionItem name="deploy-faq" summary="Can I roll back a release?">
        Yes. Open Deployments, pick any earlier build and promote it to production.
      </AccordionItem>
    </Accordion>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="max-width:420px" class="eq-accordion">
  <details name="deploy-faq" open class="eq-accordion-item">
    <summary class="eq-accordion-trigger">
      <span class="eq-accordion-title">Why did my build time out?</span>
      <svg class="eq-accordion-chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <path d="M4 6l4 4 4-4" />
      </svg>
    </summary>
    <div class="eq-accordion-content">Builds stop after 45 minutes. Cache node_modules between runs or split the job in two.</div>
  </details>
  <details name="deploy-faq" class="eq-accordion-item">
    <summary class="eq-accordion-trigger">
      <span class="eq-accordion-title">Where do environment variables live?</span>
      <svg class="eq-accordion-chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <path d="M4 6l4 4 4-4" />
      </svg>
    </summary>
    <div class="eq-accordion-content">Under Settings, Environment. Values saved there apply from the next deploy.</div>
  </details>
  <details name="deploy-faq" class="eq-accordion-item">
    <summary class="eq-accordion-trigger">
      <span class="eq-accordion-title">Can I roll back a release?</span>
      <svg class="eq-accordion-chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <path d="M4 6l4 4 4-4" />
      </svg>
    </summary>
    <div class="eq-accordion-content">Yes. Open Deployments, pick any earlier build and promote it to production.</div>
  </details>
</div>

Independent items

Leave out name and each item opens and closes on its own.

package.json
Scripts: dev, build, lint. 14 dependencies, 9 dev dependencies.
tsconfig.json
strict is on. Paths map @/* to the project root.
next.config.ts
Output is static export, images are unoptimised.
Code
import { Accordion, AccordionItem } from "@/components/equip/accordion";

export default function Example() {
  return (
    <Accordion style={{ maxWidth: 420 }}>
      <AccordionItem summary="package.json" open>
        Scripts: dev, build, lint. 14 dependencies, 9 dev dependencies.
      </AccordionItem>
      <AccordionItem summary="tsconfig.json" open>
        strict is on. Paths map @/* to the project root.
      </AccordionItem>
      <AccordionItem summary="next.config.ts">
        Output is static export, images are unoptimised.
      </AccordionItem>
    </Accordion>
  );
}
HTMLworks in any framework, with the part's CSS
<div style="max-width:420px" class="eq-accordion">
  <details open class="eq-accordion-item">
    <summary class="eq-accordion-trigger">
      <span class="eq-accordion-title">package.json</span>
      <svg class="eq-accordion-chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <path d="M4 6l4 4 4-4" />
      </svg>
    </summary>
    <div class="eq-accordion-content">Scripts: dev, build, lint. 14 dependencies, 9 dev dependencies.</div>
  </details>
  <details open class="eq-accordion-item">
    <summary class="eq-accordion-trigger">
      <span class="eq-accordion-title">tsconfig.json</span>
      <svg class="eq-accordion-chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <path d="M4 6l4 4 4-4" />
      </svg>
    </summary>
    <div class="eq-accordion-content">strict is on. Paths map @/* to the project root.</div>
  </details>
  <details class="eq-accordion-item">
    <summary class="eq-accordion-trigger">
      <span class="eq-accordion-title">next.config.ts</span>
      <svg class="eq-accordion-chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
        <path d="M4 6l4 4 4-4" />
      </svg>
    </summary>
    <div class="eq-accordion-content">Output is static export, images are unoptimised.</div>
  </details>
</div>

Props

PropTypeDefaultWhat it does
summaryReactNodenoneAccordionItem: the always-visible heading that opens and closes the item.
...restComponentProps<"div"> | ComponentProps<"details">noneAccordion takes every div attribute. AccordionItem takes every details attribute, including open, name and onToggle.

Anatomy

root.eq-accordionA plain div that draws the lines between items.
item.eq-accordion-itemThe native details element.
trigger.eq-accordion-triggerThe native summary element.
title.eq-accordion-title
chevron.eq-accordion-chevronTurns over when the item is open.
content.eq-accordion-content

Keyboard

EnterSpaceOpens or closes the focused item.
TabMoves to the next item, or into the open content.

Notes

  • Renders on the server with no client JavaScript; the browser does the opening, closing and grouping.
  • Items with the same name form an exclusive group: opening one closes the rest.
  • Find in page searches closed items and opens the one that matches in engines that support it.
  • The summary is the control, so keep links and buttons out of it; the chevron is decorative.
  • The animated open needs interpolate-size and ::details-content. Where either is missing the item opens and closes instantly, and the chevron still turns.

Source

Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.

components/equip/accordion.tsx41 lines
import type { ComponentProps, ReactNode } from "react";
import "./accordion.css";

export type AccordionProps = ComponentProps<"div">;

/** A stack of AccordionItems with a line between each. */
export function Accordion({ className, ...rest }: AccordionProps) {
  return <div {...rest} className={["eq-accordion", className].filter(Boolean).join(" ")} />;
}

export type AccordionItemProps = ComponentProps<"details"> & {
  /** The always-visible heading that opens and closes the item. */
  summary: ReactNode;
};

/**
 * One section. Give every item in a group the same `name` and the browser keeps
 * only one of them open. Pass `open` to start it expanded.
 */
export function AccordionItem({ summary, className, children, ...rest }: AccordionItemProps) {
  return (
    <details {...rest} className={["eq-accordion-item", className].filter(Boolean).join(" ")}>
      <summary className="eq-accordion-trigger">
        <span className="eq-accordion-title">{summary}</span>
        <svg
          className="eq-accordion-chevron"
          viewBox="0 0 16 16"
          fill="none"
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinecap="round"
          strokeLinejoin="round"
          aria-hidden="true"
        >
          <path d="M4 6l4 4 4-4" />
        </svg>
      </summary>
      <div className="eq-accordion-content">{children}</div>
    </details>
  );
}
components/equip/accordion.css99 lines
@layer equip {
  .eq-accordion {
    border-block: 1px solid var(--eq-line);
    font-family: var(--eq-font);
  }

  .eq-accordion-item + .eq-accordion-item {
    border-top: 1px solid var(--eq-line);
  }

  .eq-accordion-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--eq-space-3);
    min-height: var(--eq-h-m);
    padding: var(--eq-space-2) var(--eq-space-1);
    border-radius: var(--eq-radius-s);
    color: var(--eq-ink);
    font: var(--eq-weight-strong) var(--eq-text-m) / 1.3 var(--eq-font);
    list-style: none;
    cursor: pointer;
    user-select: none;
    transition:
      background-color var(--eq-dur) var(--eq-ease),
      scale var(--eq-dur) var(--eq-ease);
  }

  .eq-accordion-trigger::-webkit-details-marker {
    display: none;
  }

  @media (hover: hover) {
    .eq-accordion-trigger:hover {
      background: var(--eq-tint);
    }
  }

  .eq-accordion-trigger:focus-visible {
    outline: 2px solid var(--eq-focus);
    outline-offset: 2px;
  }

  .eq-accordion-title {
    min-width: 0;
  }

  .eq-accordion-chevron {
    flex: none;
    width: 16px;
    height: 16px;
    color: var(--eq-ink-2);
    transition: rotate var(--eq-dur-move) var(--eq-spring);
  }

  .eq-accordion-item[open] > .eq-accordion-trigger .eq-accordion-chevron {
    rotate: 180deg;
  }

  .eq-accordion-content {
    padding: 0 var(--eq-space-1) var(--eq-space-3);
    color: var(--eq-ink-2);
    font-size: var(--eq-text-s);
    line-height: 1.5;
  }

  .eq-accordion-content > :first-child {
    margin-top: 0;
  }

  .eq-accordion-content > :last-child {
    margin-bottom: 0;
  }

  /* Animated open and close where the engine can size to auto. Elsewhere it opens instantly. */
  @supports (interpolate-size: allow-keywords) and selector(::details-content) {
    .eq-accordion-item {
      interpolate-size: allow-keywords;
    }

    .eq-accordion-item::details-content {
      block-size: 0;
      overflow: clip;
      /* Closing: quick. */
      transition:
        block-size var(--eq-dur) var(--eq-ease),
        content-visibility var(--eq-dur) var(--eq-ease) allow-discrete;
    }

    /* Opening: the longer travel time, on the plain ease so the height never overshoots
       and shoves the items below it past where they settle. */
    .eq-accordion-item[open]::details-content {
      block-size: auto;
      transition:
        block-size var(--eq-dur-move) var(--eq-ease),
        content-visibility var(--eq-dur-move) var(--eq-ease) allow-discrete;
    }
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index