Create a Pure CSS Accordion with Smooth Animations - No JavaScript Required
If you’ve ever built a FAQ section that jerks open, or watched a page reload just to hide a paragraph, you know the frustration of clunky interactivity. Today’s browsers are fast enough to handle tiny animations without a single line of JavaScript, and a pure‑CSS accordion keeps your markup light, your load time low, and your code easier to maintain. Let’s dive in and build one that slides open smoothly, works on every modern device, and looks good enough to impress a client.
Why a CSS‑only accordion matters
Most tutorials reach for JavaScript the moment they need to toggle visibility. That works, but it adds extra weight, extra event listeners, and a chance for bugs. A CSS‑only solution:
- Reduces page size – no extra script files.
- Improves accessibility – you can control focus and ARIA attributes directly in HTML.
- Keeps the logic declarative – the browser does the work, you just describe the state.
I first tried a CSS accordion on a personal project for a local bakery. The owner wanted a simple “Our Story” section that could expand without any flashing or delay. I stripped out the JavaScript we had written for a previous client, and the page felt instantly snappier. The secret? Using the :checked pseudo‑class together with CSS transitions.
The basic markup
Start with a list of items. Each accordion panel needs a hidden checkbox, a label that will act as the clickable header, and a content div that will expand.
<div class="accordion">
<div class="item">
<input type="checkbox" id="acc1" class="toggle">
<label for="acc1" class="header">What is CSS?</label>
<div class="content">
<p>Cascading Style Sheets (CSS) is the language that describes how HTML elements look on screen.</p>
</div>
</div>
<div class="item">
<input type="checkbox" id="acc2" class="toggle">
<label for="acc2" class="header">Why use flexbox?</label>
<div class="content">
<p>Flexbox makes it easy to align items in a row or column, even when their size changes.</p>
</div>
</div>
<!-- Add more items as needed -->
</div>
A few things to note:
- The
inputis of typecheckbox. We hide it later, but it still receives focus and can be toggled with the keyboard. - The
labelis linked to the checkbox via theforattribute. Clicking the label checks or unchecks the box. - The
contentdiv holds the hidden text. Its height will be animated.
Styling the accordion
Now let’s write the CSS. The goal is to hide the checkbox, style the header, and animate the content’s height.
/* Hide the native checkbox */
.accordion .toggle {
position: absolute;
opacity: 0;
pointer-events: none;
}
/* Basic header style */
.accordion .header {
display: block;
padding: 0.75rem 1rem;
background: #f5f5f5;
border: 1px solid #ddd;
cursor: pointer;
font-weight: bold;
}
/* Content container – start collapsed */
.accordion .content {
max-height: 0;
overflow: hidden;
transition: max-height 0.35s ease;
background: #fff;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
border-bottom: 1px solid #ddd;
}
/* When the checkbox is checked, expand the content */
.accordion .toggle:checked + .header + .content {
max-height: 500px; /* large enough for most content */
}
/* Optional: add a little arrow indicator */
.accordion .header::after {
content: "▸";
float: right;
transition: transform 0.35s ease;
}
/* Rotate arrow when open */
.accordion .toggle:checked + .header::after {
transform: rotate(90deg);
}
Why use max-height instead of height?
CSS cannot animate from height: 0 to height: auto because the browser does not know the final size in advance. By setting a large max-height value, we give the browser a numeric endpoint it can interpolate. The value just needs to be larger than any expected content height; if you have very tall panels, bump it up a bit.
Keeping it accessible
Even though the checkbox is hidden, it still participates in the tab order because it’s focusable by default. If you prefer to keep it out of the natural flow, add tabindex="-1" to the input and rely on the label for keyboard interaction. Also, consider adding ARIA attributes:
<label for="acc1" class="header" aria-expanded="false">What is CSS?</label>
Then use a small CSS rule to toggle the attribute:
.accordion .toggle:checked + .header {
aria-expanded: true;
}
Unfortunately CSS cannot change attribute values directly, so you would need a tiny script for full ARIA compliance. In most simple cases the hidden checkbox approach is sufficient, especially when the page already follows progressive enhancement.
Adding a smooth fade
If you want the text to fade in as the panel opens, combine opacity with the height transition.
.accordion .content p {
opacity: 0;
transition: opacity 0.35s ease;
}
/* Fade in when expanded */
.accordion .toggle:checked + .header + .content p {
opacity: 1;
}
Now the paragraph fades in while the container slides down, giving a more polished feel.
Multiple panels open at once vs. single open
The checkbox method lets users open any number of panels. If you prefer an “only one open at a time” behavior, replace the checkboxes with radio buttons that share the same name attribute.
<input type="radio" name="accordion" id="acc1" class="toggle">
Now checking one radio button automatically unchecks the others, creating a classic accordion where only one section is visible.
A quick performance tip
Because we rely on max-height, the browser has to repaint the element each frame. For very long pages with many accordions, you might notice a tiny lag on low‑end devices. A workaround is to use CSS transform with scaleY instead of height, but that requires a different markup trick. For most everyday sites, the max-height approach is more than fast enough.
Wrap‑up
Building a pure CSS accordion is a great way to keep your front‑end code lean and your UI snappy. The key ingredients are:
- A hidden checkbox (or radio) to store state.
- A label that toggles the checkbox.
- A content container whose
max-heighttransitions from 0 to a large value. - Optional arrow rotation and fade‑in for extra polish.
Give it a try on your next project, and you’ll see how little JavaScript you really need for everyday interactivity. As always, experiment with colors, spacing, and timing until it feels just right for your design language. Happy coding!
- → Build Keyboard‑Friendly Anchor Navigation with Pure CSS and ARIA @toggleanchors
- → Design Systems Explained: How to Create Consistent UI Across Projects @pixelcraftstudio
- → Modern CSS Techniques: Using Variables and Calc for Dynamic Layouts @codecrafthub
- → Building a Responsive Navigation Bar with Flexbox and CSS Grid @codecrafthub
- → From Zero to Junior Frontend Engineer: An 8‑Week Practical Learning Path @devpath