Introduction
Accordions accompany us on almost every website. They compress information for us so we don’t get overwhelmed by large tsunamis of text. And while a tad unpractical, I have found a way to make them, without any JavaScript whatsover. Just so you know, I have not addressed the accessibility of this method adn there may be a few issues like no aria tags and no focusable buttons. It’s just a fun side project that I came up with and I am happy how it turned out. Anyway, if you have any ideas on how to improve that method after reading the article, you can leave a comment under the youtube video. Happy reading ✌️.
Resources
- 🔗 If you wish to assimilate this article in video format, you can go and check out my youtube clip about it.
- 🔗 Link to the finished codepen
And now let’s begin!
Crafting
First, of course, we will need a container some sections. We gotta start somewhere. I will name the container ‘accordion’ and the items - ‘accordion-item’.
<ul class="accordion">
<li class="accordion-item">Lorem ipsum</li>
<li class="accordion-item">Lorem ipsum</li>
<li class="accordion-item">Lorem ipsum</li>
</ul>
- Lorem ipsum
- Lorem ipsum
- Lorem ipsum
Now that we have our base, we must think of a way to interact with it. Without JS, there are not so many ways to interact with a native html page. We may use some css trickery like hover, but that with expand the accordion only when our mouse is on it, and mobile it would be even more terrible. No, we have to find something constant. Something that can be clicked and will stay active, until we tell it not to be. Like …🥁…
A checkbox!
It is clickable and I don’t need to write any code for that behaviour. Ok, let’s add a title to our section and a checkbox.
<ul class="accordion">
<li class="accordion-item">
<h4>I am a section</h4>
<input type="checkbox" />
Lorem ispum
</li>
<li class="accordion-item">
<h4>I am a section too</h4>
<input type="checkbox" />
Lorem ispum
</li>
<li class="accordion-item">
<h4>I am a section three</h4>
<input type="checkbox" />
Lorem ispum
</li>
</ul>
I am a section
Lorem ispum
I am a section too
Lorem ispum
I am a section three
Lorem ispum
Okay, now we have something to click on, but… It’s just a tiny square on the right of our text. The UX of that is below sea level. Ok, let’s think of a better way. Hmm… Well, we can try the label tag? From the countless forms I’ve crafted over the years, I remebered one thing. The label of the checkbox must be wired to the input with a for
attribute. That way, when you click on the label, it magically clicks the checkbox aswell. No code required. Ok, let’s make the title a label and connect it to the checkbox. I will name the label .accordion-trigger
and the input .accordion-action
,
<ul class="accordion">
<li class="accordion-item">
<label for="c1" class="accordion-trigger">I am a section</label>
<input id="c1" type="checkbox" class="accordion-action" />
<p class="accordion-content">Lorem ispum</p>
</li>
<li class="accordion-item">
<label for="c2" class="accordion-trigger">I am a section too</label>
<input id="c2" type="checkbox" class="accordion-action" />
<p class="accordion-content">Lorem ispum</p>
</li>
<li class="accordion-item">
<label for="c2" class="accordion-trigger">I am a section three</label>
<input id="c2" type="checkbox" class="accordion-action" />
<p class="accordion-content">Lorem ispum</p>
</li>
</ul>
Lorem ispum
Lorem ispum
Lorem ispum
Ok, that’s a start. When we click on the label, the checkbox is activated, which is just what we wanted. Now what? We have to figure out how to hide and unhide the text base on the state of that checkbox. That is where the sibling selector comes to the rescue.
General Sibling Selector (~)
The general sibling selector selects all elements that are next siblings of a specified element.
The following example selects all<p>
elements that are next siblings of<div>
elements:
— w3schools
div ~ p {
background-color: yellow;
}
Good, that means we can apply specific styles to our content with this selector only when the checkbox is checked. We just need to modify the above variant a little. Let’s try to make our text red.
.accordion-action:checked ~ .accordion-content {
color: red;
}
Let’s try it out! For the sake of saving screen space, I will leave only one section for now on. Click the label.
Lorem ispum
Right. So that is our starting point, now we have to hide the text initially and then show it when the checkbox is checked. But won’t that just spawn the text into existance. I mean, accordions always animate that opening super gracefully. And ours will just pop into existence. No, we have to try something else.
I know that there are 4 ways of animating something on the web.
- adding some keyframes. But that will require specific values and I have no idea how large the text will be.
- translations. I can scale the content, but that will introduce major distortions to the text and again, will need specific values.
- using JS to get the scroll height and then cycling between scroll height and 0, which is a NO-NO here.
- using a transition to go between one value and another…but again, specific values EXCEPT! We might have a breaktrough on that fourth one.
While I was having my regular 8 hour youtube marathon, I stumbled upon a video from Kevin Powell, that shows neat trick for seamless transitioning between 0 and n grid rows. You can watch his video for more detailed info, but now I will implemet the trick in our code.
I just need to set the .accordion-content
’s display to grid and it’s template rows to 0fr/1fr. The catch is that I also have to wrap the text in .accordion-content
in another div and set it’s overflow
to hidden
. Let’s do the styling now.
<div className="border p-4 rounded-[0.5rem]">
<ul class="accordion">
<li class="accordion-item">
<label for="c1" class="accordion-trigger">
I am a section. CLICK ME!
</label>
<input id="c1" type="checkbox" class="accordion-action peer" />
<div class="accordion-collapsable">
<div class="accordion-content">Lorem ispum dolor sit amet</div>
</div>
</li>
</ul>
</div>
.accordion-collapsable {
display: grid;
grid-template-rows: 0fr;
transition: all 300ms ease;
border-bottom: 1px solid #666;
}
.accordion-action:checked ~ .accordion-collapsable {
grid-template-rows: 1fr;
}
.accordion-content {
overflow: hidden;
padding: 0;
}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In pulvinar blandit pharetra. Aliquam sed urna nisl. Donec tristique libero at orci varius, id faucibus metus fermentum. Nam suscipit neque a nunc euismod, non blandit ante bibendum. Aenean maximus non dolor ut bibendum.
All right! That look great. Not let’s try to add some more sections, hide the checkbox and the list-item’s bullet and see what’s going on!
ul {
list-style: none;
}
.accordion-trigger {
cursor: pointer;
}
.accordion-item {
display: flex;
flex-direction: column;
position: relative;
}
.accordion-item:last-child .accordion-collapsable {
border: none;
}
<input id="c6" type="checkbox" class="accordion-action" hidden />
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In pulvinar blandit pharetra. Aliquam sed urna nisl. Donec tristique libero at orci varius, id faucibus metus fermentum. Nam suscipit neque a nunc euismod, non blandit ante bibendum. Aenean maximus non dolor ut bibendum.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In pulvinar blandit pharetra. Aliquam sed urna nisl. Donec tristique libero at orci varius, id faucibus metus fermentum. Nam suscipit neque a nunc euismod, non blandit ante bibendum. Aenean maximus non dolor ut bibendum.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In pulvinar blandit pharetra. Aliquam sed urna nisl. Donec tristique libero at orci varius, id faucibus metus fermentum. Nam suscipit neque a nunc euismod, non blandit ante bibendum. Aenean maximus non dolor ut bibendum.
Ok, works it works pretty well. But i wonder… Can I make it so that when I click an item, it closes every other opened item. I can’t use JS to select every opened item and close it. I must find another way. A “vanilla” way. Hmm…
Got it. I can just use a radio button. While the checkboxes can be clicked individually, radio buttons only allow one value to be selected. I just have to set the same name
attribute on every radio button, so that they are “linked”. With this method we can even know which item is selected. Let’s try it out:
<ul class="accordion">
<li class="accordion-item">
<label for="c1" class="accordion-trigger">
I am a section. CLICK ME!
</label>
<input id="c1" type="radio" name="accordion-1" class="accordion-action" />
<div class="accordion-collapsable">
<div class="accordion-content">Lorem ipsum dolor sit amet.</div>
</div>
</li>
...
</ul>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In pulvinar blandit pharetra. Aliquam sed urna nisl. Donec tristique libero at orci varius, id faucibus metus fermentum. Nam suscipit neque a nunc euismod, non blandit ante bibendum. Aenean maximus non dolor ut bibendum.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In pulvinar blandit pharetra. Aliquam sed urna nisl. Donec tristique libero at orci varius, id faucibus metus fermentum. Nam suscipit neque a nunc euismod, non blandit ante bibendum. Aenean maximus non dolor ut bibendum.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In pulvinar blandit pharetra. Aliquam sed urna nisl. Donec tristique libero at orci varius, id faucibus metus fermentum. Nam suscipit neque a nunc euismod, non blandit ante bibendum. Aenean maximus non dolor ut bibendum.
COOL! We’re almost there. Now we have to tackle one last problem. And that is that once we’ve opened the accordion, there is not way of closing it. I can break my finger clicking on it, but it won’t close. That is because radio buttons are not unselectable. The only way to reset them is to reset the form they are in with a reset button. And that is exactly what we are going to do!.
First we need to wrap our accordion in a form element. Then we need to add a reset button to every list element. The trick is to enable the reset button only when the item is opened, so that we don’t reset the form on every click:
<ul class="accordion">
<li class="accordion-item">
<label for="c1" class="accordion-trigger">
I am a section. CLICK ME!
</label>
<input id="c1" type="radio" name="accordion-1" class="accordion-action" />
<div class="accordion-collapsable">
<div class="accordion-content">Lorem ipsum dolor sit amet.</div>
</div>
<button type="reset" class="accordion-reset">
</li>
...
</ul>
.accordion-reset {
opacity: 0;
display: none;
pointer-events: none;
cursor: pointer;
}
.accordion-action:checked ~ .accordion-reset {
display: block;
position: absolute;
inset: 0;
pointer-events: auto;
}
The Result
Yee-Haw!
We did it! We have a fully functioning accordion with ZERO JavaScript. A little bit tricky, but I used it on may websites and it just works. And the end-user could not care less if there is any js used or not. Actually, if he ever stopped js from running in his dev tools, the accordion would still function.
Here is all the code, styled and ready to use:
<form method="get">
<ul class="accordion">
<li class="accordion-item">
<label class="accordion-trigger" for="item-1">
I am an accordion item
<Chevron class="accordion-svg" />
</label>
<input
name="my-accordion"
class="accordion-action"
id="item-1"
type="radio"
hidden
/>
<div class="accordion-collapsable">
<div class="accordion-content">
<p>
Cras cursus consequat lectus, et pellentesque nisl semper id.
Vivamus at tellus non erat interdum scelerisque. Pellentesque
maximus a magna eget volutpat.
</p>
</div>
</div>
<button type="reset" class="accordion-reset" />
</li>
...
</ul>
</form>
:root {
--neutral-clr: #5f9ea0;
}
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
align-items: center;
font-family: sans-serif;
height: 100vh;
background: rgb(43, 3, 46);
background: linear-gradient(
118deg,
rgba(43, 3, 46, 1) 0%,
rgba(46, 30, 75, 1) 100%
);
font-size: 1.5rem;
}
form {
background: #fff;
display: flex;
padding: 1rem;
border-radius: 0.5rem;
}
ul {
list-style: none;
}
.accordion {
max-width: 30ch;
width: 100%;
}
.accordion-item {
display: flex;
flex-direction: column;
position: relative;
& > :first-child {
margin-right: 1rem;
}
& svg {
transition: rotate 300ms ease;
width: 1rem;
}
&:last-child .accordion-collapsable {
border: none;
}
}
.accordion-trigger {
font-size: 1rem;
font-weight: 600;
display: flex;
align-items: center;
width: 100%;
justify-content: space-between;
padding: 1rem 0;
cursor: pointer;
transition: padding 0.5s ease;
&:hover {
padding-left: 0.5rem;
}
}
.accordion-collapsable {
display: grid;
grid-template-rows: 0fr;
transition: all 300ms ease;
border-bottom: 1px solid #666;
color: #464646;
font-size: 1rem;
}
.accordion-content {
overflow: hidden;
padding: 0;
}
.accordion-content :last-child {
margin-bottom: 1rem;
}
.accordion-action:checked ~ .accordion-collapsable {
grid-template-rows: 1fr;
}
.accordion-item:has(.accordion-action:checked) svg {
rotate: -180deg;
}
.accordion-reset {
opacity: 0;
display: none;
pointer-events: none;
cursor: pointer;
position: absolute;
inset: 0;
}
.accordion-action:checked ~ .accordion-reset {
display: block;
position: absolute;
inset: 0;
pointer-events: auto;
}
Bonus
Thanks for reading this article :)
If you make it this far, then you deserve this last bonus.
Tailwind version of the code! Wheee:
<form method="get" className="border p-4 rounded-[0.5rem]">
<ul class="accordion list-none">
<li
class="accordion-item list-none [&:last-child_.accordion-collapsable]:border-none relative"
>
<label for="r4" class="accordion-trigger cursor-pointer">
I am a section. CLICK ME!
</label>
<input
id="r4"
type="radio"
name="accordion-1"
class="accordion-action peer hidden"
/>
<div
class="accordion-collapsable grid grid-rows-[0fr] peer-checked:grid-rows-[1fr] transition-all border-b border-[#666]"
>
<div class="accordion-content overflow-hidden">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. In pulvinar
blandit pharetra. Aliquam sed urna nisl. Donec tristique libero at
orci varius, id faucibus metus fermentum. Nam suscipit neque a nunc
euismod, non blandit ante bibendum. Aenean maximus non dolor ut
bibendum.
</div>
</div>
<button
type="reset"
className="hidden cursor-pointer opacity-0 inset-0 peer-checked:pointer-events-auto peer-checked:absolute peer-checked:block"
/>
</li>
</ul>
</form>
See you in the next one!