Accordion
Collapsible content sections built on native <details> / <summary>.
Multi-open needs zero JS. Single-open adds a small toggle handler.
Multi-open
Default behavior — multiple items can be open simultaneously. Uses native <details>/<summary> with zero JavaScript.
Is it accessible?
Yes. It uses native HTML details/summary elements which are fully accessible by default. Screen reader and keyboard support are built in.
Is it styled?
Yes. It follows the shadcn/ui token system for consistent theming across light and dark modes.
Is it animated?
Yes. Uses ::details-content with calc-size(auto, size) for smooth height animation. The chevron rotates via CSS transition.
<div class="accordion">
<details class="accordion-item" open>
<summary class="accordion-trigger">
<span>Is it accessible?</span>
<svg class="accordion-chevron" aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
</summary>
<div class="accordion-content">
<p>Yes. It uses native HTML details/summary elements which are fully accessible by default. Screen reader and keyboard support are built in.</p>
</div>
</details>
<details class="accordion-item" open>
<summary class="accordion-trigger">
<span>Is it styled?</span>
<svg class="accordion-chevron" aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
</summary>
<div class="accordion-content">
<p>Yes. It follows the shadcn/ui token system for consistent theming across light and dark modes.</p>
</div>
</details>
<details class="accordion-item">
<summary class="accordion-trigger">
<span>Is it animated?</span>
<svg class="accordion-chevron" aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
</summary>
<div class="accordion-content">
<p>Yes. Uses ::details-content with calc-size(auto, size) for smooth height animation. The chevron rotates via CSS transition.</p>
</div>
</details>
</div>
Single-open
Only one item can be open at a time. Add data-type="single" to the wrapper — a small JS handler closes siblings on toggle.
What payment methods do you accept?
We accept all major credit cards (Visa, Mastercard, AmEx), PayPal, and bank transfers for annual plans.
Can I cancel my subscription?
Yes, you can cancel at any time from your account settings. Your access continues until the end of the current billing period.
Do you offer team pricing?
Yes. Teams of 5 or more get a 20% discount. Contact our sales team for custom enterprise plans with dedicated support.
<div class="accordion" data-type="single">
<details class="accordion-item" open>
<summary class="accordion-trigger">
<span>What payment methods do you accept?</span>
<svg class="accordion-chevron" aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
</summary>
<div class="accordion-content">
<p>We accept all major credit cards (Visa, Mastercard, AmEx), PayPal, and bank transfers for annual plans.</p>
</div>
</details>
<details class="accordion-item">
<summary class="accordion-trigger">
<span>Can I cancel my subscription?</span>
<svg class="accordion-chevron" aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
</summary>
<div class="accordion-content">
<p>Yes, you can cancel at any time from your account settings. Your access continues until the end of the current billing period.</p>
</div>
</details>
<details class="accordion-item">
<summary class="accordion-trigger">
<span>Do you offer team pricing?</span>
<svg class="accordion-chevron" aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>
</summary>
<div class="accordion-content">
<p>Yes. Teams of 5 or more get a 20% discount. Contact our sales team for custom enterprise plans with dedicated support.</p>
</div>
</details>
</div>
CSS view file
/* -- Accordion component --------------------------------------- */
@layer components {
.accordion {
display: flex;
flex-direction: column;
}
.accordion-item {
border-bottom: 1px solid var(--border);
&:last-child { border-bottom: none; }
/* -- Content animation (CSS-only) ------------------------- */
& ::details-content {
block-size: 0;
overflow-y: clip;
transition: block-size 200ms ease, content-visibility 200ms allow-discrete;
}
&[open] ::details-content {
block-size: auto;
}
}
@starting-style {
.accordion-item[open] ::details-content {
block-size: 0;
}
}
/* Remove default details marker */
.accordion-trigger {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 1rem 0;
font-size: 0.9375rem;
font-weight: 500;
text-align: left;
cursor: pointer;
list-style: none;
color: var(--foreground);
transition: color 150ms;
&::-webkit-details-marker { display: none; }
&::marker { content: ''; }
&:hover { text-decoration: underline; }
}
/* Chevron rotation */
.accordion-chevron {
color: var(--muted-foreground);
transition: transform 200ms ease;
flex-shrink: 0;
details[open] > .accordion-trigger & {
transform: rotate(180deg);
}
}
/* Content */
.accordion-content {
padding-bottom: 1rem;
font-size: 0.875rem;
line-height: 1.7;
color: var(--muted-foreground);
overflow: hidden;
& p { margin: 0; }
}
}
JavaScript view file
Only needed for single-open mode (when data-type="single" is set). Multi-open accordions use zero JavaScript — native <details> handles everything.
// -- Accordion -----------------------------------------------
// Single-open accordion behavior using native <details> elements.
function init() {
document.querySelectorAll('.accordion[data-type="single"]:not([data-init])').forEach((accordion) => {
accordion.dataset.init = '';
const items = accordion.querySelectorAll('.accordion-item');
const collapsible = accordion.hasAttribute('data-collapsible');
items.forEach((item) => {
item.addEventListener('toggle', () => {
if (item.open) {
items.forEach((sibling) => {
if (sibling !== item && sibling.open) sibling.open = false;
});
} else if (!collapsible) {
const anyOpen = Array.from(items).some((i) => i.open);
if (!anyOpen) item.open = true;
}
});
});
});
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });