Build an Accessible Accordion
Are you in the right place? If you are constrained to a third-party component library, see the Accordion Accessibility Checklist and ensure that it passes. Otherwise, follow the guidance here to choose the best option for your project.
Use the NYSDS Accordion Web Component
The NYSDS Accordion is a web component you can drop into your project and use right away. For most teams, this is the simplest way to achieve an accessible component. This means teams don’t need to manually implement accessibility or test for it.
What's already handled for you:-
Keyboard Navigation
Tab, Enter, Space, and Arrows work as intended
-
Screen reader support
Tested with NVDA, JAWS, and VoiceOver
-
Focus management
Focus stays where users expect it
-
Voice control
Supports Windows and macOS
-
Zoom magnification
Works correctly at 200% browser zoom
-
WCAG 2.2 AA conformant
Meets New York State standards
Alternatives
For projects not ready to adopt the Design System Whether you seek to remediate an existing component or build one from scratch, these reference implementations will ensure an accessible outcome. For more details, read about this pattern.
Style the native HTML details element
When to use
If your team has source-level control over UI code,
wants to avoid error-prone semantics programming,
and has no hard requirements in excess of what native HTML provides,
then the details element is a great option
that has built-in accessibility and is effectively unbreakable.
Native example
Lorem ipsum dolor sit amet./**
* NYSA11y Accordion (native)
* <details><summary>
*/
@layer nysa11y {
body:has(.nysa11y) {
details.nysa11y.accordion[data-component="accordion"][data-html="native"] {
--nysa11y-chevron-rotation-time: 0.3s;
*,
*:before,
*:after {
box-sizing: border-box;
}
font-family:
"Proxima Nova",
-apple-system,
"BlinkMacSystemFont",
"Segoe UI",
"Roboto",
"Helvetica",
"Arial",
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol";
& summary {
background-color: var(--nys-color-neutral-50, #ededed);
border-radius: 0.25rem;
color: var(--nys-color-ink, #1b1b1b);
display: flex;
font-size: var(--nys-type-size-ui-xl, 20px);
font-weight: var(--nys-font-weight-bold, 700);
gap: 0.5rem;
line-height: 1.5rem;
list-style-type: none;
padding-block: 1rem;
padding-inline: 1rem;
word-break: break-word;
&::before {
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolyline fill='%231b1b1b' points='14.724 12.486, 7.375 19.835, 7.013 20.710, 7.400 21.585, 8.275 21.960, 9.150 21.585, 16.849 13.911, 17.298 13.236, 17.448 12.486, 17.298 11.737, 16.849 11.062, 9.150 3.363, 8.262 3.000, 7.375 3.388, 7.000 4.263, 7.375 5.138, 14.724 12.486'/%3E%3C/svg%3E%0A");
background-size: contain;
content: "";
display: inline-block;
flex-shrink: 0;
height: 1.5rem;
width: 1.5rem;
transform: rotate(0deg);
transition: transform var(--nysa11y-chevron-rotation-time);
}
&:focus-visible {
outline: solid var(--nys-border-width-md, 2px)
var(--nys-color-focus, #004dd1);
outline-offset: var(--nys-space-2px, 2px);
}
&:hover,
&:focus {
background-color: var(
--nys-accordion-background-color--header--hover,
var(--nys-color-neutral-100, #d0d0ce)
);
}
&::-webkit-details-marker,
&::marker {
content: "";
display: none;
}
}
&::details-content {
opacity: 0;
transition: opacity var(--nysa11y-chevron-rotation-time)
ease-in-out;
}
&[open] {
summary {
border-end-end-radius: 0;
border-end-start-radius: 0;
&::before {
transform: rotate(90deg);
}
}
&::details-content {
background-color: var(--nys-color-ink-reverse, #ffffff);
opacity: 1;
padding-block: 1rem;
padding-inline: 1.25rem;
}
}
}
}
}
Build a custom accordion component
When to use
If your team has source-level control over UI code,
but DOES have a hard requirement not provided by native HTML,
you may choose to build a custom accordion.
Or perhaps your legacy component has been flagged for remediation
and you need a high quality reference.
This custom example recreates the accessibility built into the
NYSDS Accordion and the details element.
See the
W3C APG Accordion Pattern
for more involved implementations.
/**
* NYSA11y Accordion (custom)
* Path: /assets/nysa11y/accordion-custom.css
* Depends on /assets/nysa11y/accordion-custom.js
*/
@layer nysa11y {
body:has(.nysa11y) {
div.nysa11y.accordion[data-component="accordion"][data-html="custom"] {
--nysa11y-chevron-rotation-time: 0.3s;
*,
*:before,
*:after {
box-sizing: border-box;
}
font-family:
"Proxima Nova",
-apple-system,
"BlinkMacSystemFont",
"Segoe UI",
"Roboto",
"Helvetica",
"Arial",
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol";
& button[data-part="summary"] {
background-color: var(--nys-color-neutral-50, #ededed);
border: none;
border-radius: 0.25rem;
color: var(--nys-color-ink, #1b1b1b);
display: flex;
font-family: inherit;
font-size: var(--nys-type-size-ui-xl, 20px);
font-weight: var(--nys-font-weight-bold, 700);
gap: 0.5rem;
line-height: 1.5rem;
list-style-type: none;
padding-block: 1rem;
padding-inline: 1rem;
width: 100%;
word-break: break-word;
&::before {
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' width='24' height='24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpolyline fill='%231b1b1b' points='14.724 12.486, 7.375 19.835, 7.013 20.710, 7.400 21.585, 8.275 21.960, 9.150 21.585, 16.849 13.911, 17.298 13.236, 17.448 12.486, 17.298 11.737, 16.849 11.062, 9.150 3.363, 8.262 3.000, 7.375 3.388, 7.000 4.263, 7.375 5.138, 14.724 12.486'/%3E%3C/svg%3E%0A");
background-size: contain;
content: "";
display: inline-block;
flex-shrink: 0;
height: 1.5rem;
width: 1.5rem;
transform: rotate(0deg);
transition: transform var(--nysa11y-chevron-rotation-time);
}
&:focus-visible {
outline: solid var(--nys-border-width-md, 2px)
var(--nys-color-focus, #004dd1);
outline-offset: var(--nys-space-2px, 2px);
}
&:hover,
&:focus {
background-color: var(
--nys-accordion-background-color--header--hover,
var(--nys-color-neutral-100, #d0d0ce)
);
}
}
& div[data-part="content"] {
height: 0;
opacity: 0;
transition: opacity var(--nysa11y-chevron-rotation-time)
ease-in-out;
visibility: hidden;
}
&.is-open {
button[data-part="summary"] {
border-end-end-radius: 0;
border-end-start-radius: 0;
&::before {
transform: rotate(90deg);
}
}
div[data-part="content"] {
background-color: var(--nys-color-ink-reverse, #ffffff);
height: auto;
opacity: 1;
padding-block: 1rem;
padding-inline: 1.25rem;
visibility: visible;
}
}
}
}
}
/**
* NYSA11y Accordion (custom)
* Path: /assets/nysa11y/accordion-custom.js
* Depends on /assets/nysa11y/accordion-custom.css
*/
const nysa11y = window.nysa11y || {};
class Accordion {
#selectorToggle =
'[data-component="accordion"].nysa11y [data-part="summary"]';
#selectorParent = '[data-component="accordion"].nysa11y';
#stateOpen = "is-open";
constructor(options = {}) {
this.container = options.container || document;
this.init();
}
init() {
this.toggles = this.container.querySelectorAll(this.#selectorToggle);
if (!this.toggles.length) return;
this.toggles.forEach((toggle) => {
// Avoid binding the same event listener multiple times
toggle.removeEventListener("click", this.#handleToggle);
toggle.addEventListener("click", this.#handleToggle);
// Set initial accessibility state
const isExpanded = toggle
.closest(this.#selectorParent)
?.classList.contains(this.#stateOpen);
toggle.setAttribute("aria-expanded", isExpanded || false);
});
}
#handleToggle = (event) => {
const toggleElement = event.currentTarget;
const parent = toggleElement.closest(this.#selectorParent);
if (!parent) return;
const isOpen = parent.classList.toggle(this.#stateOpen);
toggleElement.setAttribute("aria-expanded", isOpen);
};
}
nysa11y.Accordion = Accordion;
document.addEventListener("DOMContentLoaded", () => {
new nysa11y.Accordion();
});
About this pattern
An accordion is a component that presents a binary state button and an adjacent content area.
The button toggles the visibility of the content.
This component is also known as a "disclosure" or "expander."
The native HTML analog is the details element.
The Header
The accordion button is commonly called the "header,"
since it functions as a descriptive heading for the content.
The native analog is the summary element.
Because it is activated to use the component, the header MUST be focusable,
and its visible label MUST either be the accessible name or be part of
the accessible name.
Note that the details element
automagically gains its accessible name from the text of its
summary.
The entire header should function semantically as a button. As such it MUST meet all rules for button accessibility. Because it is an interactive element, the header MUST NOT contain interactive child elements, otherwise it is a nested-interactive WCAG failure.
The binary state (open/expanded vs. closed/collapsed) is inherited
"for free" when using details and
summary.
In a custom component it is recreated with aria-expanded
and JavaScript.
The Content
When content is hidden, it MUST be fully hidden from assistive technologies,
not merely visually hidden.
The details element properly hides content by default.
Note that the native content area is selected in CSS with the
::details-content pseudo element.
Best Practices When multiple accordions are composed into a group, some implementations allow mutually exclusive activation, meaning that only one may be open at a time. Many Accessibility professionals consider this to be an anti-pattern. Consider, for example, the use case of comparing content between two accordions. If mutually exclusive behavior is provided, please consider giving the user an option to disable it.