Skip to main content Accessibility Feedback

Creating a progressively enhanced accordion with the details and summary elements (and 11 lines of JavaScript)

The details and summary elements create a simple show/hide disclosure element with only HTML. Today, I wanted to show you how you can use a touch of JavaScript to extend them into a progressively enhanced accordion component.

Let’s dig in!

How details and summary work

You put your entire accordion content inside a details element. The heading that should act as a toggle goes inside a summary element.

<details>
	<summary>Toggle me</summary>
	I'm the content
</details>

Toggle me I’m the content

Because it’s just HTML, the details and summary elements are progressively enhanced by default. Browsers that support them get the interactivity, but older browsers see the content full expanded and accessible.

When the component is open or expanded, it has an open attribute on it. You can also add the open attribute to make your accordion expanded by default.

<details open>
	<summary>Toggle me, too</summary>
	I'm open by default.
</details>

Toggle me, too I’m open by default.

Creating an accordion group

We can create a simple, progressively enhanced accordion group with a collection of details and summary elements.

<details>
	<summary>Merlin</summary>
	Dancing Teacups
</details>

<details>
	<summary>Ursula</summary>
	Stealing Voices
</details>

<details>
	<summary>Radagast</summary>
	Talks to Animals
</details>

With accordions, its common for only one item to be open at a time. Opening a collapsed component closes any currently open component. With a bit of JavaScript, we can expand our collection of details and summary elements to add this functionality.

To make this into a component group, let’s wrap our elements in a parent with a [data-accordion] attribute.

<div data-accordion>
	<details>...</details>
	<details>...</details>
	<details>...</details>
</div>

Detecting toggle events

When a details element is opened or closed, it emits a toggle event. We can listen for it with the addEventListener() method.

The toggle event doesn’t bubble, so we need to set the optional third argument, useCapture, to true.

document.addEventListener('toggle', toggleHandler, true);

The toggle event fires when a details element is opened or closed. We only need to run our code if an element was opened.

We’ll use the Element.hasAttribute() method to check for the open attribute on the element that was toggled, the event.target. If the element doesn’t have the attribute, we’ll return to end our function early.

/**
 * Handle toggle events
 * @param  {Event} event The Event object
 */
function toggleHandler (event) {

	// Only run if accordion is open
	if (!event.target.hasAttribute('open')) return;

}

We also only want to run our code if the toggled element is inside a [data-accordion] wrapper element.

Let’s use the Element.closest() method to look for a parent element with the [data-accordion] attribute. If no matching element is found, we’ll again use the return operator to end early.

/**
 * Handle toggle events
 * @param  {Event} event The Event object
 */
function toggleHandler (event) {

	// Only run if accordion is open
	if (!event.target.hasAttribute('open')) return;

	// Only run on accordions inside our selector
	let parent = event.target.closest('[data-accordion]');
	if (!parent) return;

}

Closing open elements

We can use the Element.querySelectorAll() method to look for details elements with the [open] attribute inside the parent element, and assign them to the opened variable.

Then, we’ll use a for...of loop to loop through each open accordion.

/**
 * Handle toggle events
 * @param  {Event} event The Event object
 */
function toggleHandler (event) {

	// Only run if accordion is open
	if (!event.target.hasAttribute('open')) return;

	// Only run on accordions inside our selector
	let parent = event.target.closest('[data-accordion]');
	if (!parent) return;

	// Get all open accordions inside parent
	let opened = parent.querySelectorAll('details[open]');

	// Close open ones that aren't current accordion
	for (let accordion of opened) {
		// ...
	}

}

We can use the Element.removeAttribute() method to remove the [open] attribute and close the details element.

But, we don’t want to close the one that was just opened. We can use the strict equals operator (===) to check if the current accordion is the event.target. If so, we’ll use the continue operator to skip to the next item instead.

/**
 * Handle toggle events
 * @param  {Event} event The Event object
 */
function toggleHandler (event) {

	// ...

	// Close open ones that aren't current accordion
	for (let accordion of opened) {
		if (accordion === event.target) continue;
		accordion.removeAttribute('open');
	}

}

Now, we have a simple, progressively enhanced accordion component with just a few lines of JS.

Here’s a demo.