- Published on
The Ultimate Guide to Building Pure CSS Accordions (No JavaScript!)
- Authors
- Name
- Md Nasim Sheikh
- @nasimStg
The Ultimate Guide to Building Pure CSS Accordions (No JavaScript!)
Learn how to create responsive, accessible, and beautiful accordion components using only HTML and CSS. This comprehensive guide covers modern techniques with <details>
and the classic checkbox hack.
Table of Contents
- The Ultimate Guide to Building Pure CSS Accordions (No JavaScript!)
- What Exactly is an Accordion?
- Method 1: The Modern & Semantic Way with <details> and <summary>
- Step 1: The HTML Structure
- Step 2: Foundational CSS Styling
- Step 3: Creating a Custom +/- Icon
- Step 4: Adding a Smooth Opening Animation
- Method 2: The Classic "Checkbox Hack"
- Step 1: The HTML Structure
- Step 2: The CSS Magic
- Step 3: Adding the +/- Icon
- Advanced Technique: A "Single Open" Accordion
- HTML for Single-Open Accordion
- Final Polish & Best Practices
- Conclusion
Accordions are one of the most versatile UI components in a web developer's toolkit. They allow you to pack a large amount of content into a small space, revealing information progressively as the user needs it. This is fantastic for FAQs, product feature lists, multi-step forms, and so much more. It cleans up the interface and reduces cognitive load for your users.
Many tutorials will jump straight to JavaScript to build this functionality. But what if I told you that you can build a fully functional, accessible, and even animated accordion using only HTML and CSS?
In this comprehensive guide, we'll dive deep into two powerful, JavaScript-free methods for creating accordions. We'll start with the modern, semantic approach and then explore a classic hack for broader compatibility or different use cases. By the end, you'll be an expert in building lean, performant accordions that look great and work everywhere.
Ready? Let's get building!
What Exactly is an Accordion?
Before we write any code, let's align on the core concept. An accordion consists of a stack of headers that can be clicked (or tapped) to reveal or hide their associated content panel.
Key characteristics include:
- Header (or Trigger): The always-visible part that the user interacts with. It usually contains a title and an icon (like a
+
or>
). - Panel (or Content): The collapsible section that contains the detailed information.
- State: Each accordion item is either expanded (open) or collapsed (closed).
Our goal is to create this mechanism using the simplest, most robust tools available: native HTML and CSS.
<details>
and <summary>
Method 1: The Modern & Semantic Way with If you're building for the modern web, this is the method you should reach for 99% of the time. HTML5 introduced the <details>
and <summary>
elements, which were literally designed for this exact purpose.
Why is this approach so great?
- Zero JavaScript: It works right out of the box.
- Built-in Accessibility: It's keyboard-navigable by default. Screen readers understand its state (expanded/collapsed) without any extra work from you.
- Clean & Semantic HTML: Your code clearly communicates its intent to both browsers and other developers.
- Graceful Degradation: In very old browsers that don't support it, the content simply shows up as expanded, meaning the content is still accessible.
Step 1: The HTML Structure
The HTML for a <details>
-based accordion is beautifully simple. The <summary>
tag becomes your header, and any other content inside <details>
becomes your collapsible panel.
Let's create a simple three-item accordion:
<div class="accordion-container">
<details>
<summary>What is semantic HTML?</summary>
<div class="accordion-content">
<p>Semantic HTML refers to the use of HTML tags that convey meaning and structure about the content they contain, rather than just its presentation. Elements like `<article>`, `<section>`, `<nav>`, and `<header>` are examples of semantic tags.</p>
</div>
</details>
<details>
<summary>Why is CSS `box-sizing: border-box` useful?</summary>
<div class="accordion-content">
<p>By default, the `width` and `height` properties in CSS apply only to the content box. `box-sizing: border-box;` changes this so that `width` and `height` include the content, padding, and border. This makes creating layouts much more intuitive and predictable.</p>
</div>
</details>
<details open> <!-- This one will be open by default -->
<summary>What is the CSS `::before` pseudo-element?</summary>
<div class="accordion-content">
<p>The `::before` pseudo-element allows you to insert content onto a page from CSS (without it needing to be in the HTML). While the end result is not actually in the DOM, it appears on the page as if it were. It's perfect for adding decorative icons or labels.</p>
</div>
</details>
</div>
Notice a few things here:
- The
<summary>
tag is the first child of<details>
. This is required. - We can add the
open
attribute to a<details>
tag to make it expanded by default. - I've wrapped the panel content in a
div
with a class. This isn't strictly necessary but will become very helpful when we add animations later.
Step 2: Foundational CSS Styling
Out of the box, this is functional but looks a bit plain. Let's add some CSS to make it look like a proper accordion.
/* Basic Reset & Body Styles */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f4f7f6;
color: #333;
padding: 2rem;
}
.accordion-container {
max-width: 700px;
margin: 0 auto;
}
/* Style the accordion items */
details {
background-color: #fff;
border: 1px solid #d1d1d1;
border-radius: 5px;
margin-bottom: 10px;
overflow: hidden; /* Important for border-radius to work with children */
}
/* Style the summary (the clickable header) */
summary {
padding: 1rem 1.5rem;
font-weight: 600;
cursor: pointer;
position: relative; /* For positioning the icon */
outline: none; /* Remove default focus outline, we'll add our own */
}
/* Add a better focus state for accessibility */
summary:focus-visible {
outline: 2px solid blue;
outline-offset: 2px;
}
/* Style the content panel */
.accordion-content {
padding: 0 1.5rem 1rem 1.5rem;
border-top: 1px solid #d1d1d1;
color: #555;
line-height: 1.6;
}
This CSS gives us a clean, card-based design. The cursor: pointer
on the <summary>
provides a visual cue that it's clickable. We've also added a custom focus-visible
state, which is a modern best practice for accessibility, ensuring keyboard users get a clear outline without it showing up on every mouse click.
Step 3: Creating a Custom +/- Icon
The default triangle/arrow icon provided by the browser is functional, but it's often not what we want aesthetically. Let's replace it with a classic plus/minus icon that toggles with the state.
We can do this with a CSS pseudo-element!
/* Hide the default marker/arrow */
summary {
list-style: none; /* For Firefox */
}
summary::-webkit-details-marker {
display: none; /* For Chrome/Safari */
}
/* Add our custom icon */
summary::after {
content: '+';
font-size: 1.5rem;
font-weight: 400;
position: absolute;
right: 1.5rem;
top: 50%;
transform: translateY(-50%);
transition: transform 0.2s;
}
/* Change the icon when the accordion is open */
details[open] > summary::after {
content: '−'; /* A proper minus sign */
}
This is the magic right here.
- We first hide the browser's default arrow icon.
- We use the
::after
pseudo-element on the<summary>
to add our+
sign. - The key selector is
details[open] > summary::after
. The[open]
is an attribute selector. It targets the<summary>
only when its parent<details>
element has theopen
attribute (which the browser adds/removes automatically on click). - When open, we change the
content
to a minus sign.
Step 4: Adding a Smooth Opening Animation
A sharp jump from closed to open can be jarring. A smooth transition elevates the user experience significantly. However, you can't transition height
from 0
to auto
, which is a classic CSS challenge.
Fear not! We can use a modern CSS Grid trick to achieve this.
/* Update our content panel styles for animation */
.accordion-content {
/* Remove previous padding and border */
color: #555;
line-height: 1.6;
/* Add Grid properties */
display: grid;
grid-template-rows: 0fr; /* Initially collapsed */
transition: grid-template-rows 0.3s ease-out;
}
/* We need an inner wrapper for padding */
.accordion-content > p {
padding: 0 1.5rem 1rem 1.5rem;
border-top: 1px solid #d1d1d1;
overflow: hidden; /* Crucial for the grid animation */
}
/* When the accordion is open, expand the grid row */
details[open] > .accordion-content {
grid-template-rows: 1fr;
}
This is a bit more advanced, so let's break it down:
- We set the content panel (
.accordion-content
) todisplay: grid
. - We control its visibility with
grid-template-rows
. When collapsed, it's0fr
(zero fraction units, effectively zero height). When open, it's1fr
(one fraction unit, it will take up all available space). - We can now apply a
transition
to thegrid-template-rows
property! - Crucially, for this to work, the direct child of the grid container needs
overflow: hidden
. This is why we added that extra<p>
or<div>
inside our content panel in the HTML. The padding and border now go on this inner element.
And there you have it! A beautiful, animated, accessible, and semantic accordion with zero JavaScript.
Method 2: The Classic "Checkbox Hack"
While <details>
is the future, you might occasionally need an alternative. The "Checkbox Hack" is a clever, time-tested technique that uses a hidden checkbox, a <label>
, and CSS sibling selectors to manage state.
When would you use this?
- You need to support very old browsers where
<details>
isn't available. - You need more complex control, like ensuring only one accordion item can be open at a time (which we'll touch on later).
This method is less semantic, as we're using form elements for something that isn't a form. It's a trade-off between semantics and functionality in certain contexts.
Step 1: The HTML Structure
The structure is more complex. We need an <input type="checkbox">
, a <label>
linked to it, and the content panel. The <label>
will be our visible, clickable header.
<div class="accordion-container-hack">
<div class="accordion-item-hack">
<input type="checkbox" id="accordion-hack-1" class="accordion-input" />
<label for="accordion-hack-1" class="accordion-label-hack">What is the Checkbox Hack?</label>
<div class="accordion-content-hack">
<p>The Checkbox Hack is a CSS technique that uses a hidden checkbox (`<input type="checkbox">`) and its associated `<label>` to toggle the visibility of an adjacent element. Clicking the label changes the checkbox's `:checked` state, which can be used in CSS selectors to apply different styles.</p>
</div>
</div>
<div class="accordion-item-hack">
<input type="checkbox" id="accordion-hack-2" class="accordion-input" checked /> <!-- Open by default -->
<label for="accordion-hack-2" class="accordion-label-hack">What is a CSS Sibling Selector?</label>
<div class="accordion-content-hack">
<p>A sibling selector targets elements that share the same parent. The Adjacent Sibling Selector (`+`) targets the element immediately following another. The General Sibling Selector (`~`) targets all siblings that follow. These are crucial for the Checkbox Hack to work.</p>
</div>
</div>
</div>
Key points:
- Each accordion item is wrapped in its own
div
. - The
id
of the checkbox must match thefor
attribute of the label. This is how clicking the label toggles the checkbox. - The
checked
attribute on the input makes an item open by default.
Step 2: The CSS Magic
Here's where the "hack" comes into play. We'll hide the checkbox itself and use its :checked
state to show the content.
.accordion-item-hack {
background-color: #fff;
border: 1px solid #d1d1d1;
border-radius: 5px;
margin-bottom: 10px;
overflow: hidden;
}
/* Hide the actual checkbox */
.accordion-input {
display: none;
}
/* Style the label to look like a header */
.accordion-label-hack {
display: block; /* Make it take up the full width */
padding: 1rem 1.5rem;
font-weight: 600;
cursor: pointer;
position: relative;
}
/* Style and hide the content panel */
.accordion-content-hack {
max-height: 0; /* Collapse the content */
overflow: hidden;
transition: max-height 0.3s ease-out, padding 0.3s ease-out;
padding: 0 1.5rem; /* Remove padding when collapsed */
}
/* THE MAGIC: Show content when the checkbox is checked */
.accordion-input:checked + .accordion-label-hack + .accordion-content-hack {
max-height: 500px; /* An arbitrary large value */
padding: 1rem 1.5rem;
}
Let's dissect the most important selector: .accordion-input:checked + .accordion-label-hack + .accordion-content-hack
.accordion-input:checked
: This targets our checkbox only when it is checked.+ .accordion-label-hack
: The+
is the adjacent sibling selector. It looks for the label that comes immediately after the checked checkbox.+ .accordion-content-hack
: It then looks for the content panel that comes immediately after that label.
When all these conditions are met, we apply the styles to expand the content. We transition max-height
to create a smooth animation. You must set max-height
to a value larger than your content will ever be.
Step 3: Adding the +/- Icon
The process is very similar to the <details>
method, but we target the label's pseudo-element and change it based on the input:checked
state.
.accordion-label-hack::after {
content: '+';
font-size: 1.5rem;
position: absolute;
right: 1.5rem;
top: 50%;
transform: translateY(-50%);
}
.accordion-input:checked + .accordion-label-hack::after {
content: '−';
}
Advanced Technique: A "Single Open" Accordion
A common requirement is for only one accordion item to be open at a time. Clicking a new item should close the previously opened one. This is difficult with <details>
in pure CSS, but it's surprisingly easy with a variation of our hack!
Simply replace <input type="checkbox">
with <input type="radio">
and give all the radio buttons in the group the same name
attribute.
HTML for Single-Open Accordion
<div class="accordion-container-hack">
<div class="accordion-item-hack">
<!-- Note: type="radio" and the same name="accordion-group" -->
<input type="radio" name="accordion-group" id="accordion-radio-1" class="accordion-input" />
<label for="accordion-radio-1" class="accordion-label-hack">First Item</label>
<div class="accordion-content-hack">
<p>Content for the first item.</p>
</div>
</div>
<div class="accordion-item-hack">
<input type="radio" name="accordion-group" id="accordion-radio-2" class="accordion-input" />
<label for="accordion-radio-2" class="accordion-label-hack">Second Item</label>
<div class="accordion-content-hack">
<p>Content for the second item.</p>
</div>
</div>
</div>
Because only one radio button in a named group can be checked at a time, clicking a new label will automatically un-check the previous one, closing its associated panel. The CSS remains exactly the same as the checkbox hack! It's an incredibly elegant solution.
Final Polish & Best Practices
Accessibility First: While the
<details>
element is highly accessible, always ensure your<summary>
and labels have clear, descriptive text. For the checkbox/radio hacks, the link between the label and input is crucial. Never forget thefor
andid
attributes.Respect User Preferences: Some users are sensitive to motion. You can use the
prefers-reduced-motion
media query to disable your animations for them.@media (prefers-reduced-motion: reduce) { .accordion-content, .accordion-content-hack { transition: none; } }
Test, Test, Test: Check your accordion on different browsers and devices. Ensure it works with keyboard navigation (Tab to focus, Enter/Space to toggle) and with screen readers.
Conclusion
You are now equipped with two powerful, robust, and JavaScript-free methods for building one of the web's most common UI patterns.
We've seen that for modern development, the <details>
and <summary>
elements are the clear winner, offering unparalleled simplicity, semantics, and accessibility right out of the box. For more complex state management like a "single-open" accordion or for legacy browser support, the classic Checkbox/Radio Hack remains a brilliantly clever tool in your CSS arsenal.
The next time you need to organize content cleanly, you can confidently build a lean, fast, and beautiful accordion without writing a single line of JavaScript.
What will you build with this technique? Share your creations or ask questions in the comments below!