Skip to main content Accessibility Feedback

Creating a progessively enhanced accordion with Web Components

Last year, I wrote about how to create a progressively enhanced accordion with a few lines of vanilla JavaScript.

Today, I wanted to revisit that approach using Web Components. Let’s dig in!

How it works

The details and summary elements provide a browser native disclosure component.

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

In the article I wrote last year, we used some DOM manipulation to listen for whenever a details element was expanded and close all of the others in a group.

Today, let’s convert that over to a Web Component.

Creating the custom element

Let’s name our custom element pe-accordion (for Progressively Enhanced Accordion). We’ll wrap it around a collection of details and summary elements.

<pe-accordion>
	<details open>
		<summary>Merlin</summary>
		Dancing Teacups
	</details>

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

	<details>
		<summary>Radagast</summary>
		Talks to Animals
	</details>
</pe-accordion>

By default, the UI will show a collection of elements that can be independently expanded and collapsed in modern browsers, and will render as plain old text in older ones.

Defining our Web Component

Next, we’ll use the customElements.define() method to define our Web Component.

We’ll pass in the pe-accordion element and a class that extends the HTMLElement object as arguments.

customElements.define('pe-accordion', class extends HTMLElement {
	// ...
});

Inside our class, we’ll use the constructor() method to create our Web Component instance. We’ll run the super() method to make sure we inherit the parent class properties.

customElements.define('pe-accordion', class extends HTMLElement {

	constructor () {

		// Inherit class properties
		super();

	}

});

Now, we have a defined Web Component that does nothing. Let’s add some interactivity!

Creating our listener

Inside the constructor, let’s define an event handler property on the instance. We want to access this inside the event handler function, so we’ll cache it to the instance variable first.

I copy/pasted this from our previous script, and made a few modifications.

We don’t need to check for the parent element, since we’ll scope our listener to the custom element. And we’ll search for all opened accordions on the instance. Otherwise, this is the same script as before.

constructor () {

	// Inherit class properties
	super();

	// Cache instance for use in function
	let instance = this;

	// Setup handler function
	this.handler = function (event) {

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

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

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

	};

}

Next, I added two addition lifecycle methods to the Web Component.

The connectedCallback() method runs when the element is attached to the DOM, and the disconnectedCallback() method runs if it’s removed.

In the connectedCallback() method, I listen for toggle events on the custom pe-accordion element (this), and pass in this.handler as the function to run. The toggle event doesn’t bubble, so I need to pass in true for the optional useCapture parameter.

/**
 * Runs each time the element is appended to or moved in the DOM
 */
connectedCallback () {
	this.addEventListener('toggle', this.handler, true);
}

In the disconnectedCallback() method, I stop listening for events.

/**
 * Runs when the element is removed from the DOM
 */
disconnectedCallback () {
	this.removeEventListener('toggle', this.handler, true);
}

Styling

A nice thing about custom elements is that they provide a simple styling hook.

In our case, let’s give our details elements a border between them and a bit of space. We’ll also add a slight and heavier font weight to the summary element.

pe-accordion details {
	padding: 0.5em 0;
}

pe-accordion details:not(:last-child) {
	border-bottom: 1px solid #808080;
}

pe-accordion summary {
	font-weight: bold;
	margin-bottom: 0.25em;
}

Putting it all together

Now, I have a custom Web Component that progressively enhances details and summary elements into an accordion group.

Here’s a demo you can play with.