Semantics and UX of Pure CSS3 Accordions

There are many examples online of pure CSS3 accordions. Many of these accordions use a combination of anchor elements with the :target pseudo-class while others use a combination of labels and checkboxes with the :checked pseudo-class. Either approach is certainly functional, but which is the right way? Perhaps the better question would be is there a right way? I believe the answer can be found in the underlying semantics and UX.

Exploring the Semantics

Let's take a moment to look first at chosen elements for the task. On one hand, the anchor tag, and on the other, the checkbox input. These are two fundamentally different elements with different purposes. It's important to mention this because I'm a big fan of using elements for their intended purpose.

Checkboxes, as other input elements, are meant to capture on/off (Boolean) data in a form. A single checkbox might be used to capture a confirmation (such as agreeing to Terms and Conditions). Multiple checkboxes might be used to capture an array of similar items. A newsletter form, for example, may ask a user to select one or more categories or topics in which the user has interest.

Anchor elements (<a>) are intended to direct the user to view specific content located elsewhere on the current page, on another page, or to download files. The fundamental purpose is to provide the user with a path to specific content. That said, what is the purpose of an accordion? It's meant to display specific content to the user when clicking the accordion title. This is why I believe the anchor element is the most logical choice.


The Code

<div class="accordion">
  <h3 class="accordion__title">
    <a href="">Accordion 1</a>
  </h3>
  <div class="accordion__content"></div>
</div>
<div class="accordion">
  <h3 class="accordion__title">
    <a href="">Accordion 2</a>
  </h3>
  <div class="accordion__content"></div>
</div>
<div class="accordion">
  <h3 class="accordion__title">
    <a href="">Accordion 3</a>
  </h3>
  <div class="accordion__content"></div>
</div>

This is a good start — simple, meaningful, and has context. Now let's build on it.

For :target to work, the anchors href needs to link to an id which will be at the parent .accordion element. Placing the id on the .accordion element rather than the .accordion__title will allow us access to style all child elements of the active (or :target) accordion to indicate the state. For now, the focus of our active and inactive states serve to display (or not display) the accordion's content. Let's add some placeholder content within .accordion__content. Well also add some basic CSS to compliment our markup.

HTML

<div id="accordion-1" class="accordion">
  <h3 class="accordion__title">
    <a href="#accordion-1">Accordion 1</a>
  </h3>
  <div class="accordion__content"></div>
</div>
<div id="accordion-2" class="accordion">
  <h3 class="accordion__title">
    <a href="#accordion-2">Accordion 2</a>
  </h3>
  <div class="accordion__content"></div>
</div>
<div id="accordion-3" class="accordion">
  <h3 class="accordion__title">
    <a href="#accordion-3">Accordion 3</a>
  </h3>
  <div class="accordion__content"></div>
</div>

CSS

body {
  font-family: 'Helvetica', sans-serif;
  color: #555;
}
.accordion__title {
  display: block;
  font-weight: 600;
  margin: 0 0 10px 0;
}
.accordion__title > a {
  color: #555;
  text-decoration: none;
  position: relative;
  padding-left: 26px;
}
.accordion__title > a:before {
  color: #777;
  content: "\25B6";
  font-size: 14px;
  font-weight: bold;
  line-height: 1;
  height: 16px;
  width: 16px;
  text-align: center;
  position: absolute;
  left: 0;
  top: 4px;
}
.accordion__content {
  display: none;
  border-top: 1px solid #777;
  border-bottom: 1px solid #777;
  margin-bottom: 20px;
  margin-left: 26px;
  padding: 5px 0;
}
.accordion:target .accordion__title > a:before {
  transform: rotate(90deg);
}
.accordion:target .accordion__content {
  display: block;
}

View example in JSFiddle...

What we end up with is a pretty basic implementation. It's certainly functional, notably regarding the use of display: block; and display: none;, but this simply won't do for our final demo.


Building Better User Experience

Before diving into improving the UX of our accordion, I'd like to mention another reason for our decision to use the :target pseudo-class. The purpose of the accordion is to offer the user a list of titles to peruse. The user then clicks on a title of interest to reveal the content. Case in point would be a FAQs page.

Fundamentally, this isn't much different from clicking a title from a list of news articles to load its detail page. The main difference is in the amount of content that is to be displayed. In the case of the accordion, it's typically not enough content merit its own page. Back on point, the other reason for choosing :target is actually the exact opposite of the reason that some implementations use checkboxes and labels with the :checked pseudo-class. Similar to loading a single detail page from a list of article titles, displaying one item at a time is the reasoning behind :target.

Why do I think this important? By displaying one item at a time, we avoid having users click each title while also preventing a large, ever-growing list of unrelated content blocks from being shown. That's bad user experience.

We don't need to stop there.

Our accordion needs some work still, though we're close. So, what else do we need to do to create a better experience? For one, let's work on something better than the sudden displaying and hiding of content. We want to show the user that the accordion is opening and closing.

Motion helps to provide a certain level of depth and tangibility to the UI which, in turn, enhances a user's understanding of the what's happening. We're going to make use of CSS3 transitions to show the accordion open and close.

.accordion__title a:before {
  ...
  transition-property: transform;
  transition-duration: 0.25s;
}
.accordion__content {
  margin-left: 26px;
  margin-bottom: 0;
  padding: 0;
  height: 0;
  overflow: scroll;
  transition-property: margin, padding, height;
  transition-duration: 0.25s;
}
.accordion:target .accordion__content {
  border-top: 1px solid #777;
  border-bottom: 1px solid #777;
  margin-bottom: 20px;
  padding: 5px 0;
  height: 250px;
}

View example in JSFiddle...

Excellent! We have a pure CSS3 accordion with absolutely no JavaScript. We've set all properties affecting the vertical height of the accordion to 0 for the closed (default) state and set them to their desired value in the open (:target) state. Since the border properties are only 1px and aren't listed in the transition-property, we've just moved them to only render in the open state.

We could certainly call it a day. However, there's one final wrinkle to iron out first: height. It's not dynamic, meaning that it won't change based on the height of the content. What do we do now? Do we add some JavaScript and forfeit the "pure CSS3" claim?

Nope, not at all.

Though, why make the height dynamic? Honestly, 250px would be overkill for 100px of content and, contrarily, having 250px of content in a 100px space causes the inner content to scroll. Scrolling down a web page and suddenly hitting a block of scrollable content is a bit aggravating. And, for what ever reason, I equate it to running through the jungle and hitting a patch of quicksand despite my lack personal experience with quicksand. It can also be said that one would likely not die from the aggravation of scrolling... I hope.

So, let's make the height dynamic. Wait, why couldn't we just use modifier classes for various sizes such as accordion--sm, accordion--md, etc? We certainly could use modifier classes. Though, how many sizes would we need and how/where would they be managed? Also, how would we reliably implement these classes in a dynamic web page or template? We would end up with a bunch of superfluous CSS and run into the same issue as the content height changes. We can implement a dynamic height easily without JavaScript or modifier classes simply by using max-height. We'll replace all instances of height with max-height.

.accordion__content {
  margin-left: 26px;
  margin-bottom: 0;
  padding: 0;
  max-height: 0;
  overflow: hidden;
  transition-property: margin, padding, max-height;
  transition-duration: 0.25s;
}
.accordion:target .accordion__content {
  border-top: 1px solid #777;
  border-bottom: 1px solid #777;
  margin-bottom: 20px;
  padding: 5px 0;
  max-height: 300px;
}

View example in JSFiddle...

And voila! By using max-height we can set our desired value for our content and it will size itself based on the height of the .accordion__content element. If we desire, we could even add min-height to set a minimum acceptable height for the open state, but that's completely up to you.

  • Development