In this new tutorial, we’ll learn how to build an animated JavaScript accordion component with overlapping panels.
We won’t focus so much on accessibility in this tutorial, so exploring how to make this component more accessible would be a valid next step.
Here’s what we’re going to create (click on a panel to test the behavior):
Inside a container, we’ll place a list of panels.
Each panel will have a title and content. Within the title, we’ll add a Close button from where we can close the active panel.
Here’s the required structure:
1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
By default, all panels will be collapsed.
To prevent this behavior, we’ve to assign the active
class to one or more panels like this:
There’s also the option to have more than one panel open simultaneously without one collapsing when the other is open. To enable this, we should add the data-multiple="true"
attribute to the accordion wrapper like this:
1 |
|
Let’s now concentrate on the key styles—most of the other styles aren’t anything special, so let’s leave them for now:
height: 0
and overflow: hidden
. Then, as we’ll see later, through JavaScript, we’ll recalculate their height and reveal them smoothly. Just, note that we’ll also use height: 0 !important
to reset the height to 0 and override the JavaScript styles for a previously active panel.
cursor: pointer
to all panels. On the contrary, when a panel is open, we can close it only via the close button. At this moment, only this button will have cursor: pointer
while the panel will have cursor: default
.
Here’s a part of the required styles:
1 |
/*CUSTOM STYLES HERE*/
|
2 |
|
3 |
.accordion-wrapper li { |
4 |
padding: 0 20px 100px; |
5 |
cursor: pointer; |
6 |
border-top-left-radius: var(--accordion-radius); |
7 |
border-top-right-radius: var(--accordion-radius); |
8 |
background: var(--accordion-bg-color); |
9 |
transition: all 0.2s ease-out; |
10 |
}
|
11 |
|
12 |
.accordion-wrapper li:not(:first-child) { |
13 |
margin-top: -100px; |
14 |
border-top: 2px solid var(--light-cyan); |
15 |
}
|
16 |
|
17 |
.accordion-wrapper li:nth-last-child(2), |
18 |
.accordion-wrapper li:last-child { |
19 |
border-bottom-left-radius: var(--accordion-radius); |
20 |
border-bottom-right-radius: var(--accordion-radius); |
21 |
}
|
22 |
|
23 |
.accordion-wrapper li:last-child { |
24 |
padding-bottom: 0; |
25 |
}
|
26 |
|
27 |
.accordion-wrapper:not([data-multiple="true"]) li.active { |
28 |
border-top-color: var(--accordion-active-bg-color); |
29 |
}
|
30 |
|
31 |
.accordion-wrapper li.active { |
32 |
cursor: default; |
33 |
color: var(--white); |
34 |
background: var(--accordion-active-bg-color); |
35 |
}
|
36 |
|
37 |
.accordion-wrapper li:not(.active) .accordion-content { |
38 |
height: 0 !important; |
39 |
}
|
40 |
|
41 |
.accordion-wrapper .accordion-content { |
42 |
height: 0; |
43 |
overflow: hidden; |
44 |
transition: height 0.3s; |
45 |
}
|
46 |
|
47 |
.accordion-wrapper .inner { |
48 |
padding-bottom: 40px; |
49 |
}
|
50 |
|
51 |
@media (min-width: 700px) { |
52 |
.accordion-wrapper li { |
53 |
padding-left: 60px; |
54 |
padding-right: 60px; |
55 |
}
|
56 |
|
57 |
.accordion-wrapper .inner { |
58 |
max-width: 85%; |
59 |
}
|
60 |
}
|
The way we’ll animate each panel and achieve a slide effect similar to jQuery’s slideToggle()
function is by taking advantage of the scrollHeight
property.
This property measures the height of an element’s content, including content not visible on the screen due to overflow. In our case, we’ll need to calculate that value for the .accordion-content
elements which have height: 0
and overflow: hidden
by default.
As a first action, when the DOM is ready, we’ll check if there are any active panels, and if so, we’ll set the height for the .accordion-content
element of each active panel equal to its scrollHeight
property value.
Here’s the related JavaScript code:
1 |
const activeItems = accordionWrapper.querySelectorAll("li.active"); |
2 |
|
3 |
if (activeItems) { |
4 |
activeItems.forEach(function (item) { |
5 |
const content = item.querySelector(".accordion-content"); |
6 |
content.style.height = `${content.scrollHeight}px`; |
7 |
});
|
8 |
}
|
Next, each time we click on a panel, we’ll do the following things:
active
class and ignoring all the next steps.
active
class to that panel.
.accordion-content
element of this panel equal to its scrollHeight
property value.
Here’s the JavaScript code that implements all that behavior:
1 |
const accordionWrapper = document.querySelector(".accordion-wrapper"); |
2 |
const items = accordionWrapper.querySelectorAll("li"); |
3 |
const multiple_open = accordionWrapper.dataset.multiple; |
4 |
const ACTIVE_CLASS = "active"; |
5 |
|
6 |
items.forEach(function (item) { |
7 |
item.addEventListener("click", function (e) { |
8 |
// 1
|
9 |
const target = e.target; |
10 |
if ( |
11 |
(target.tagName.toLowerCase() === "button" || target.closest("button")) && |
12 |
item.classList.contains(ACTIVE_CLASS) |
13 |
) { |
14 |
item.classList.remove(ACTIVE_CLASS); |
15 |
return; |
16 |
}
|
17 |
|
18 |
// 2
|
19 |
if ( |
20 |
"true" !== multiple_open && |
21 |
document.querySelector(".accordion-wrapper li.active") |
22 |
) { |
23 |
document
|
24 |
.querySelector(".accordion-wrapper li.active") |
25 |
.classList.remove(ACTIVE_CLASS); |
26 |
}
|
27 |
|
28 |
// 3
|
29 |
item.classList.add(ACTIVE_CLASS); |
30 |
|
31 |
// 4
|
32 |
const content = item.querySelector(".accordion-content"); |
33 |
content.style.height = `${content.scrollHeight}px`; |
34 |
});
|
35 |
});
|
Done! I hope you enjoyed the JavaScript accordion we built and learned one or two new things.
Before closing, let’s recall our main creation today:
As always, thanks a lot for reading!