komal.nakrani
Browse Themes
ThemesCategoriesDocsBlogTips & Tricks
Components & Islands••By Komal Nakrani

An Accessible Astro Accordion Component

Build an Astro accordion two ways: zero-JS with details and summary, or a button-based version with aria-expanded. Copy-paste code, styles and when to use each.

#Astro#Components#Accordion#Accessibility

The simplest Astro accordion uses the native <details> and <summary> elements: no JavaScript, keyboard support built in, and the browser’s find-in-page can open collapsed sections. When you need more control, such as only one panel open at a time across browsers or custom animation, use a button with aria-expanded and a few lines of script. Both versions are below, ready to paste.

Key Takeaways

  • <details>/<summary> is accessible and works with JavaScript disabled.
  • Give several <details> the same name attribute for “one open at a time” in supporting browsers.
  • The button version gives exact control and works the same everywhere.
  • Wrap the panels in a heading structure so screen-reader users can navigate between them.

Version 1: details and summary (no JavaScript)

---
// src/components/Accordion.astro
interface Props {
  title: string;
  open?: boolean;
  group?: string;   // same group = only one open at a time (supporting browsers)
}
const { title, open = false, group } = Astro.props;
---
<details class="accordion" open={open} name={group}>
  <summary class="accordion-summary">
    <span>{title}</span>
    <svg class="accordion-icon" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
      <path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="2" />
    </svg>
  </summary>
  <div class="accordion-body">
    <slot />
  </div>
</details>

<style>
  .accordion { border-bottom: 1px solid var(--color-border, #e5e4df); }
  .accordion-summary {
    display: flex; justify-content: space-between; align-items: center;
    padding: 1rem 0; cursor: pointer; font-weight: 600; list-style: none;
  }
  .accordion-summary::-webkit-details-marker { display: none; }
  .accordion-icon { transition: transform 0.2s ease; flex-shrink: 0; }
  .accordion[open] .accordion-icon { transform: rotate(180deg); }
  .accordion-body { padding: 0 0 1rem; }

  @media (prefers-reduced-motion: reduce) {
    .accordion-icon { transition: none; }
  }
</style>

Usage:

---
import Accordion from '../components/Accordion.astro';
---
<Accordion title="How long does delivery take?" group="faq">
  <p>Access to the repository is sent within a few minutes of purchase.</p>
</Accordion>
<Accordion title="Can I use it for client work?" group="faq">
  <p>Yes, the licence covers sites for paying clients.</p>
</Accordion>

The name attribute on <details> creates an exclusive group in current browsers: opening one closes the others. Older browsers simply allow several open at once, which is a safe fallback.

Version 2: button with aria-expanded

Use this when you need identical behaviour everywhere or want to animate height.

---
// src/components/AccordionItem.astro
interface Props { title: string; id: string; headingLevel?: 2 | 3 | 4 }
const { title, id, headingLevel = 3 } = Astro.props;
const Heading = `h${headingLevel}`;
---
<div class="acc-item">
  <Heading class="acc-heading">
    <button class="acc-trigger" type="button" aria-expanded="false" aria-controls={`${id}-panel`} id={`${id}-trigger`}>
      {title}
    </button>
  </Heading>
  <div class="acc-panel" id={`${id}-panel`} role="region" aria-labelledby={`${id}-trigger`} hidden>
    <slot />
  </div>
</div>

<style>
  .acc-heading { margin: 0; }
  .acc-trigger {
    width: 100%; text-align: left; padding: 1rem 0; font: inherit; font-weight: 600;
    background: none; border: 0; border-bottom: 1px solid var(--color-border, #e5e4df); cursor: pointer;
  }
  .acc-trigger:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
  .acc-panel { padding: 1rem 0; }
</style>

<script>
  document.querySelectorAll<HTMLButtonElement>('.acc-trigger').forEach((btn) => {
    btn.addEventListener('click', () => {
      const open = btn.getAttribute('aria-expanded') === 'true';
      btn.setAttribute('aria-expanded', String(!open));
      document.getElementById(btn.getAttribute('aria-controls')!)!.hidden = open;
    });
  });
</script>

Each item needs a unique id. For content collections, the entry id works well.

Which version to use

Need Version
FAQ section, no special behaviour details/summary
Works with JavaScript disabled details/summary
Find-in-page should reveal hidden text details/summary
Exact same behaviour in every browser Button
Custom open/close animation Button
Only one open at a time, everywhere Button (add closing logic)

Adding FAQ schema

If the accordion holds questions and answers, add FAQPage structured data so search engines understand it; the FAQ section with schema component does both.

Script-versus-island trade-offs are covered in island or script, and more component patterns in the Astro components guide. The WAI-ARIA accordion pattern documents the button version’s accessibility model.

FAQ

Is details/summary accessible?

Yes. Browsers expose it as an expandable control with keyboard support. Keep the summary text descriptive.

Can I animate details/summary?

Partly. Current browsers support animating to height: auto with interpolate-size; elsewhere it opens instantly, which is fine.