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 input is of type checkbox. We hide it later, but it still receives focus and can be toggled with the keyboard.
  • The label is linked to the checkbox via the for attribute. Clicking the label checks or unchecks the box.
  • The content div 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:

  1. A hidden checkbox (or radio) to store state.
  2. A label that toggles the checkbox.
  3. A content container whose max-height transitions from 0 to a large value.
  4. 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!

Reactions
Do you have any feedback or ideas on how we can improve this page?