Collapsing and expanding HTML elements using (mostly) CSS
Posted by Simon Larsén in Programming
Sections that collapse and expand at the click of a button is fairly ubiquitous across the web nowadays. It's especially handy for mobile, where the display is much smaller than your typical computer monitor. In this article, I'll walk you through how to create a basic collapsible content-area using almost only CSS, along with a few lines of close-to-trivial JavaScript. The focus is on CSS, not JavaScript, so you should be able to follow this even with the most rudimentary programming experience.
The fictional sidebar
For this toy example, we will be creating a collapsible div (it could really
be just about any element) that can be collapsed and expanded by clicking a
"trigger". Just for the purpose of showing the effects more clearly, we'll do
it inside of another div element, which we'll imagine is a sidebar of a
website (like the sidebar with recent posts and tags on this site). Here's the
markup for the sidebar:
<div class="sidebar">
<!- our content goes in here! -->
</div>
And the CSS:
.sidebar {
width: 30%;
border: black solid 2px;
}
This is really not important for this demo, I just mention it so that you don't wonder about some unknown HTML and CSS in the final demo. Now, let's fill that sidebar up with some content.
<div class="sidebar">
<div id="trigger">Cool content heading</div>
<div id="content">
<p>
This content would be neat to hide and show at the click of a button!
</p>
</div>
</div>
We have 3 div tags in total: one is the container (sidebar) which really has
little to do with this article. The second is a heading for the content, which
will act as the trigger for showing and hiding the content. The third is the
one containing the content that we want to hide/show (a single paragraph).
Let's get to it! Note the id attributes on the two inner div tags. When I
later refer to the #trigger, I mean the div with id="trigger", and likewise
for the #content. The id attributes serve no other purpose here, you can
remove them if you wish and everything will still work as expected.
Collapsing and expanding the div
To collapse and expand #content, we will use two classes: collapse-trigger
and collapse. The basic idea is this:
- An element with the
collapseis hidden by default. - If a
collapseelement follows an element with thecollapse-triggerAND theactiveclasses, thecollapseis visible.
You can probably guess where to put the classes in the markup already:
<div class="sidebar">
<div id="trigger" class="collapse-trigger">Cool content heading</div>
<div id="content" class="collapsible">
<p>
This content would be neat to hide and show at the click of a button!
</p>
</div>
</div>
For the CSS, fulfilling point 1 above (collapsible hidden by default) is
simple:
.collapsible {
display: none;
}
This will simply not display the element. But how do we fulfill the second
requirement? We can use the
adjacent sibling combinator
(+). It's a selector combinator that allows us to match some element, only if
it is immediately preceded by some other element. For example, the selector h1
+ p will match any p tag that is immediately preceded by an h1 tag:
<h1>This is a heading</h1>
<p>This paragraph will be matched by "h1 + p"</p>
So, to show our collapsible when it is directly preceded by a
collapse-trigger AND active element, we do this:
.collapse-trigger.active + .collapsible {
display: block;
}
We display it as a block here, but one could use other display modes as well,
depending on what visual effect is sought. Note that we are chaining two
classes in the left hand side of the + combinator, which means that an
element matches only if it's class attribute contains both of those classes
(and possibly more of them). The order of the classes is however not important,
i.e. .active.collapse-trigger would be equivalent.
That's actually all there is to it, as far as the CSS goes. Now, we can
collapse and expand the #content by opening the developer tools (F12 in
Firefox and Chrome) and manually assigning the active class to #trigger.
But that's not very convenient in every day use. This is where we need the
tiniest bit of JavaScript to be able to toggle active.
Toggling the active class with the click of a button
What we want to do is to remove and add the active class from any
collapse-trigger element by clicking it. Here, we need JavaScript, because
there is no way to change the class of an element with only CSS. For every
collapse-trigger in the page, we need to attach an event listener that
toggles the active class every time the element is clicked. It can be done
like this:
function attachCollapseTriggers() {
var colTriggers = document.getElementsByClassName("collapse-trigger");
for (var colTrig of colTriggers) {
colTrig.addEventListener("click", function() {
this.classList.toggle("active");
});
}
}
Essentially, we use the getElementsByClassName DOM method to find all
elements with the collapse-trigger class. Then, we iterate over those
elements, and add an event listener to it. The first argument to
addEventListener is an event (in this case a button click). The second
argument is a callback function, for which we provide an anonymous function. If
you have a hard time wrapping your head around anonymous functions, this will
accomplish the same thing:
function collapseTrigger() {
this.classList.toggle("active");
}
function attachCollapseTriggers() {
var colTriggers = document.getElementsByClassName("collapse-trigger");
for (var colTrig of colTriggers) {
colTrig.addEventListener("click", collapseTrigger);
}
}
When the element is clicked, the collapseTrigger function is called. Of
course, we need to call attachCollapseTriggers sometime after the page has
loaded for this to take effect. And that's it for the JavaScript, clicking the
#trigger will now cause #content to collapse and expand! However, it's not
very clear to the user that the #trigger even can be clicked. Let's make that
just a little bit more clear by adding some visual cues.
Finishing touches using the ::after pseudo class
A typical visual cue that a drop down can be expanded is a down-triangle (▼).
An up-triangle (▲) is as recognizable a cue that a menu can be collapsed. The
down-triangle should be appended to any collapse-trigger that is not active,
while the up-triangle should be appended to any collapse-trigger that also
has the active class. We can do that simply use the ::after pseudo class.
.collapse-trigger::after {
content: "▼";
float: right; /* float to the right-hand side of the content box */
}
.collapse-trigger.active::after {
content: "▲";
}
So, what happens here, exactly? When a collapse-trigger is not active, it
doesn't match the .collapse-trigger.active selector, so the content will
simply be the down-triangle. When a collapse-trigger is active, it will
match both selectors. However, .collapse-trigger.active is more specific than
.collapse-trigger, so it wins out, and the content will be an up-triangle.
And that's it, all done!
Code listing and JSFiddle link
The full code is available in the following subsections, and you can find a JSFiddle here.
Markup
<!- the outer div with the class "sidebar" isn't important, it's just any container -->
<div class="sidebar">
<div class="collapse-trigger">Cool content heading</div>
<div class="collapsible">
<p>
This content would be neat to hide and show at the click of a button!
</p>
</div>
</div>
CSS
/* the sidebar class is just an arbitrary container for this example */
.sidebar {
width: 30%;
padding: 1em;
border: black solid 2px;
}
.collapse-trigger::after {
content: "▼";
float: right;
}
.collapse-trigger.active::after {
content: "▲";
}
.collapsible {
display: none;
}
.collapse-trigger.active + .collapsible {
display: block;
}
JavaScript
function attachCollapseTriggers() {
var colTriggers = document.getElementsByClassName("collapse-trigger");
for (var colTrig of colTriggers) {
colTrig.addEventListener("click", function() {
this.classList.toggle("active");
});
}
}
attachCollapseTriggers();