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 samenameattribute 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.