Accordion
Material Design 3 accordion component for expandable content panels
Native Disclosure
Native details[open] is authoritative: .open and .accordion-item-open do
not override native open/closed state. Each item can expand independently.
Native disclosure
Delivery
Returns
<div class="accordion">
<details class="accordion-item" name="demo-delivery">
<summary class="accordion-header">Delivery</summary>
<div class="accordion-content"><div class="accordion-body"><div class="accordion-body-inner">Ships in two days.</div></div></div>
</details>
<details class="accordion-item" name="demo-delivery">
<summary class="accordion-header">Returns</summary>
<div class="accordion-content"><div class="accordion-body"><div class="accordion-body-inner">Returns within 30 days.</div></div></div>
</details>
</div>Shared name makes an exclusive native group in browsers supporting grouped
details. Without that support items remain independent; if exclusivity is
required, provide an application controller. Omit name for independent items.
Controlled Disclosure
For controlled markup, .open is canonical and .accordion-item-open remains
a compatibility alias. Classes only style the owning item’s direct panel.
CSS does not update ARIA or move focus. Initialize closed panels with hidden
and inert; keep the trigger outside the panel. Use a unique panel ID.
function setExpanded(item, expanded) {
const trigger = item.querySelector(':scope > .accordion-header');
const panel = item.querySelector(':scope > .accordion-content');
if (!expanded && panel.contains(document.activeElement)) trigger.focus();
trigger.setAttribute('aria-expanded', String(expanded));
trigger.setAttribute('aria-controls', panel.id);
item.classList.toggle('open', expanded);
item.classList.remove('accordion-item-open');
panel.hidden = !expanded;
panel.inert = !expanded;
}
// Bind trigger clicks. For exclusive groups, close direct sibling items using
// the same setter before opening this item. Do not select nested items.
Restore focus to the trigger before hiding a panel containing focus. Preserve
normal Tab navigation rather than trapping focus. The older framework/class
examples below illustrate composition; controllers must implement this complete
state contract, including hidden/inert on initial render.
Accordion
Accordions allow users to show and hide sections of related content on a page. @duskmoon-dev/core provides a complete set of Material Design 3 accordion variants.
Basic Usage
Basic Accordion
<div class="accordion">
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">What is Material Design?</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Material Design is a design system created by Google to help teams build high-quality digital experiences.
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">How do I install?</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Install using npm: npm install @duskmoon-dev/core
</div>
</div>
</div>
</div>Variants
Default Accordion
The default accordion has a clean, minimal appearance with bottom borders separating items.
Default Accordion
<div class="accordion">
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Section 1</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Content for section 1
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Section 2</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Content for section 2
</div>
</div>
</div>
</div>Filled Accordion
Filled accordions have a background color and are separated by spacing.
Filled Accordion
<div class="accordion accordion-filled">
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Filled Section 1</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Content with filled background
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Filled Section 2</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Content with filled background
</div>
</div>
</div>
</div>Outlined Accordion
Outlined accordions have a border around the entire container.
Outlined Accordion
<div class="accordion accordion-outlined">
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Outlined Section 1</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Content within outlined container
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Outlined Section 2</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Content within outlined container
</div>
</div>
</div>
</div>Separated Accordion
Separated accordions display each item as an individual card with spacing between them.
Separated Accordion
<div class="accordion accordion-separated">
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Card 1</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Each item is a separate card
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Card 2</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Each item is a separate card
</div>
</div>
</div>
</div>Density
Compact
Compact accordions have reduced padding for space-efficient layouts.
Compact Accordion
<div class="accordion accordion-compact">
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Compact Section</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Compact content with less padding
</div>
</div>
</div>
</div>Comfortable
Comfortable accordions have increased padding for more spacious layouts.
Comfortable Accordion
<div class="accordion accordion-comfortable">
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Comfortable Section</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Comfortable content with more padding
</div>
</div>
</div>
</div>Color Variants
Color variants change the text color of expanded items.
Primary
Primary Accordion
<div class="accordion accordion-primary">
<div class="accordion-item accordion-item-open">
<button class="accordion-header">
<span class="accordion-title">Primary Accordion</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
The header text turns primary color when expanded
</div>
</div>
</div>
</div>Secondary
Secondary Accordion
<div class="accordion accordion-secondary">
<div class="accordion-item accordion-item-open">
<button class="accordion-header">
<span class="accordion-title">Secondary Accordion</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
The header text turns secondary color when expanded
</div>
</div>
</div>
</div>Tertiary
Tertiary Accordion
<div class="accordion accordion-tertiary">
<div class="accordion-item accordion-item-open">
<button class="accordion-header">
<span class="accordion-title">Tertiary Accordion</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
The header text turns tertiary color when expanded
</div>
</div>
</div>
</div>Icons
Expansion Icon
The accordion icon rotates 180 degrees when the item is expanded.
Accordion with Expansion Icon
<div class="accordion">
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Section with Icon</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
The chevron icon rotates when expanded
</div>
</div>
</div>
</div>Leading Icons
Add custom icons at the start of accordion headers.
Accordion with Leading Icon
<div class="accordion">
<div class="accordion-item">
<button class="accordion-header accordion-header-icon">
<svg class="accordion-header-icon-leading" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="accordion-title">Information</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Content with a leading icon
</div>
</div>
</div>
</div>States
Open State
Add the accordion-item-open class to expand an item by default.
Open and Closed States
<div class="accordion">
<div class="accordion-item accordion-item-open">
<button class="accordion-header">
<span class="accordion-title">Open by Default</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
This section is expanded by default
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Closed by Default</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
This section is collapsed by default
</div>
</div>
</div>
</div>Disabled State
Disable an accordion item to prevent interaction.
Disabled Accordion
<div class="accordion">
<div class="accordion-item accordion-item-disabled">
<button class="accordion-header">
<span class="accordion-title">Disabled Section</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
This content cannot be accessed
</div>
</div>
</div>
</div>Without Animation
Remove animations for reduced motion preferences or performance.
Accordion Without Animation
<div class="accordion accordion-no-animation">
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">No Animation</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Expands instantly without animation
</div>
</div>
</div>
</div>Single vs Multiple Expansion
Single Expansion
Only one item can be open at a time (requires JavaScript).
Single Expansion Accordion
<div class="accordion" data-accordion="single">
<div class="accordion-item">
<button class="accordion-header" onclick="toggleAccordion(this)">
<span class="accordion-title">Section 1</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Opening this closes other sections
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header" onclick="toggleAccordion(this)">
<span class="accordion-title">Section 2</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Opening this closes other sections
</div>
</div>
</div>
</div>
<script>
function toggleAccordion(button) {
const accordion = button.closest('.accordion');
const item = button.closest('.accordion-item');
const isSingle = accordion.dataset.accordion === 'single';
const expanded = !item.classList.contains('open') && !item.classList.contains('accordion-item-open');
const set = (target, open) => {
const trigger = target.querySelector(':scope > .accordion-header');
const panel = target.querySelector(':scope > .accordion-content');
if (!open && panel.contains(document.activeElement)) trigger.focus();
panel.id ||= 'demo-panel-' + crypto.randomUUID();
trigger.setAttribute('aria-controls', panel.id);
trigger.setAttribute('aria-expanded', String(open));
target.classList.toggle('open', open);
target.classList.remove('accordion-item-open');
panel.hidden = !open;
panel.inert = !open;
};
if (isSingle && expanded) accordion.querySelectorAll(':scope > .accordion-item:not(details)').forEach(other => {
if (other !== item) set(other, false);
});
set(item, expanded);
}
</script>Multiple Expansion
Multiple items can be open simultaneously (requires JavaScript).
Multiple Expansion Accordion
<div class="accordion" data-accordion="multiple">
<div class="accordion-item">
<button class="accordion-header" onclick="toggleAccordion(this)">
<span class="accordion-title">Section 1</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Multiple sections can be open
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header" onclick="toggleAccordion(this)">
<span class="accordion-title">Section 2</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Multiple sections can be open
</div>
</div>
</div>
</div>Combining Variants
You can combine different modifiers for custom layouts.
Combined Variants
<!-- Filled, comfortable, primary accordion -->
<div class="accordion accordion-filled accordion-comfortable accordion-primary">
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Combined Variants</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
This accordion is filled, comfortable, and uses primary color
</div>
</div>
</div>
</div>
<!-- Separated, compact, secondary accordion -->
<div class="accordion accordion-separated accordion-compact accordion-secondary">
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Combined Variants</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
This accordion is separated, compact, and uses secondary color
</div>
</div>
</div>
</div>Best Practices
Content Organization
Use accordions to organize related content sections:
Organized Content with Icons
<div class="accordion accordion-separated">
<div class="accordion-item">
<button class="accordion-header accordion-header-icon">
<svg class="accordion-header-icon-leading" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="accordion-title">Account Information</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Manage your account settings and preferences
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header accordion-header-icon">
<svg class="accordion-header-icon-leading" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<span class="accordion-title">Privacy & Security</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Configure your privacy and security settings
</div>
</div>
</div>
</div>Accessibility
- Use semantic HTML with
<button>elements for headers - Implement proper ARIA attributes for screen readers
- Ensure keyboard navigation support
- Provide clear labels for all sections
Accessible Accordion
<div class="accordion">
<div class="accordion-item">
<button
class="accordion-header"
aria-expanded="false"
aria-controls="panel-1"
id="accordion-header-1"
>
<span class="accordion-title">Accessible Section</span>
<span class="accordion-icon" aria-hidden="true">▼</span>
</button>
<div
class="accordion-content"
id="panel-1"
role="region"
aria-labelledby="accordion-header-1"
>
<div class="accordion-body">
Content with proper ARIA attributes
</div>
</div>
</div>
</div>Avoid Overuse
Don’t use accordions when:
- Content should be immediately visible
- There are only 1-2 items
- Users need to compare information across sections
Progressive Disclosure
Use accordions to progressively reveal complex information:
Progressive Disclosure
<div class="accordion accordion-outlined">
<div class="accordion-item accordion-item-open">
<button class="accordion-header">
<span class="accordion-title">Basic Settings</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Most commonly used settings
</div>
</div>
</div>
<div class="accordion-item">
<button class="accordion-header">
<span class="accordion-title">Advanced Settings</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content">
<div class="accordion-body">
Advanced options for power users
</div>
</div>
</div>
</div>Framework Examples
React
import { useId, useState } from 'react';
interface AccordionItemProps {
title: string;
children: React.ReactNode;
icon?: React.ReactNode;
}
function AccordionItem({ title, children, icon }: AccordionItemProps) {
const [isOpen, setIsOpen] = useState(false);
const panelId = useId();
return (
<div className={`accordion-item ${isOpen ? 'accordion-item-open' : ''}`}>
<button
className="accordion-header"
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-controls={panelId}
>
{icon && <span className="accordion-header-icon-leading">{icon}</span>}
<span className="accordion-title">{title}</span>
<span className="accordion-icon">▼</span>
</button>
<div className="accordion-content" id={panelId} hidden={!isOpen} inert={!isOpen}>
<div className="accordion-body">
{children}
</div>
</div>
</div>
);
}
interface AccordionProps {
variant?: 'default' | 'filled' | 'outlined' | 'separated';
density?: 'default' | 'compact' | 'comfortable';
color?: 'primary' | 'secondary' | 'tertiary';
children: React.ReactNode;
}
export function Accordion({
variant = 'default',
density = 'default',
color,
children
}: AccordionProps) {
const classes = [
'accordion',
variant !== 'default' && `accordion-${variant}`,
density !== 'default' && `accordion-${density}`,
color && `accordion-${color}`
].filter(Boolean).join(' ');
return (
<div className={classes}>
{children}
</div>
);
}
// Usage
<Accordion variant="filled" color="primary">
<AccordionItem title="Section 1">
Content for section 1
</AccordionItem>
<AccordionItem title="Section 2">
Content for section 2
</AccordionItem>
</Accordion>
Vue
<template>
<div :class="accordionClasses">
<slot />
</div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
variant: {
type: String,
default: 'default',
validator: (value) => ['default', 'filled', 'outlined', 'separated'].includes(value)
},
density: {
type: String,
default: 'default',
validator: (value) => ['default', 'compact', 'comfortable'].includes(value)
},
color: {
type: String,
validator: (value) => ['primary', 'secondary', 'tertiary'].includes(value)
}
});
const accordionClasses = computed(() => {
const classes = ['accordion'];
if (props.variant !== 'default') classes.push(`accordion-${props.variant}`);
if (props.density !== 'default') classes.push(`accordion-${props.density}`);
if (props.color) classes.push(`accordion-${props.color}`);
return classes.join(' ');
});
</script>
<!-- AccordionItem.vue -->
<template>
<div :class="itemClasses">
<button
class="accordion-header"
@click="toggle"
:aria-expanded="isOpen"
:aria-controls="panelId"
>
<span v-if="$slots.icon" class="accordion-header-icon-leading">
<slot name="icon" />
</span>
<span class="accordion-title">{{ title }}</span>
<span class="accordion-icon">▼</span>
</button>
<div class="accordion-content" :id="panelId" :hidden="!isOpen" :inert="!isOpen">
<div class="accordion-body">
<slot />
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, useId } from 'vue';
const props = defineProps({
title: {
type: String,
required: true
},
defaultOpen: {
type: Boolean,
default: false
}
});
const isOpen = ref(props.defaultOpen);
const panelId = useId(); // Vue 3.5+; older applications must supply a unique ID.
const itemClasses = computed(() => {
return isOpen.value ? 'accordion-item accordion-item-open' : 'accordion-item';
});
function toggle() {
isOpen.value = !isOpen.value;
}
</script>
<!-- Usage -->
<Accordion variant="filled" color="primary">
<AccordionItem title="Section 1">
Content for section 1
</AccordionItem>
<AccordionItem title="Section 2">
Content for section 2
</AccordionItem>
</Accordion>
API Reference
Container Classes
| Class | Description |
|---|---|
.accordion |
Base accordion container (required) |
.accordion-filled |
Filled variant with background color |
.accordion-outlined |
Outlined variant with border |
.accordion-separated |
Card-style separated items |
.accordion-compact |
Reduced padding |
.accordion-comfortable |
Increased padding |
.accordion-primary |
Primary color for expanded items |
.accordion-secondary |
Secondary color for expanded items |
.accordion-tertiary |
Tertiary color for expanded items |
.accordion-no-animation |
Disable animations |
Item Classes
| Class | Description |
|---|---|
.accordion-item |
Individual accordion item (required) |
.accordion-item-open |
Expanded state |
.accordion-item-disabled |
Disabled state |
Header Classes
| Class | Description |
|---|---|
.accordion-header |
Clickable header/trigger (required) |
.accordion-header-icon |
Container for header with leading icon |
.accordion-header-icon-leading |
Leading icon in header |
.accordion-title |
Title text container |
.accordion-icon |
Expansion indicator icon |
Content Classes
| Class | Description |
|---|---|
.accordion-content |
Collapsible content wrapper (required) |
.accordion-body |
Content body with padding (required) |
Class Combinations
Class Combinations
<!-- Filled, comfortable, primary accordion -->
<div class="accordion accordion-filled accordion-comfortable accordion-primary">
<!-- items -->
</div>
<!-- Separated, compact, secondary accordion -->
<div class="accordion accordion-separated accordion-compact accordion-secondary">
<!-- items -->
</div>
<!-- Outlined, no animation accordion -->
<div class="accordion accordion-outlined accordion-no-animation">
<!-- items -->
</div>