Chapter 5: Accordion
How to create a proper accordion (also called collapsible sections, expand/collapse panels, FAQ style sections, etc.) from the very beginning.
What we want to achieve
An accordion is a group of panels where:
- Only one panel is usually open at a time (classic behavior)
- Clicking a header opens its content and closes others
- Clicking an already open header closes it
- Smooth open/close animation
- Looks good on mobile and desktop
- Is accessible (keyboard + screen reader friendly)
We’ll build three versions — from simple to more polished:
- Pure HTML + CSS only (no JavaScript – good for very simple cases)
- HTML + CSS + minimal JavaScript (most common & flexible)
- Modern version with nice animations & accessibility (what you’ll see in 2025–2026 professional sites)
Let’s go step by step.
Version 1 – Pure CSS Accordion (no JavaScript)
This version uses the :checked pseudo-class + hidden radio/checkbox trick.
HTML
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<div class="accordion"> <input type="radio" name="accordion" id="item1" checked> <label for="item1" class="accordion-header"> What is your return policy? </label> <div class="accordion-content"> <p>You can return any item within 30 days of purchase if it's unused and in original packaging.</p> </div> <input type="radio" name="accordion" id="item2"> <label for="item2" class="accordion-header"> Do you ship internationally? </label> <div class="accordion-content"> <p>Yes, we ship to most countries. Shipping fees are calculated at checkout.</p> </div> <input type="radio" name="accordion" id="item3"> <label for="item3" class="accordion-header"> How long does delivery take? </label> <div class="accordion-content"> <p>Domestic orders usually arrive in 3–7 business days. International orders take 7–20 days depending on location.</p> </div> </div> |
CSS
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
.accordion { max-width: 700px; margin: 2rem auto; font-family: system-ui, sans-serif; } .accordion input[type="radio"] { position: absolute; opacity: 0; pointer-events: none; } /* Header styling */ .accordion-header { display: block; background: #f8f9fa; padding: 1rem 1.5rem; font-weight: 600; cursor: pointer; border-bottom: 1px solid #e0e0e0; user-select: none; transition: background 0.2s; } .accordion-header:hover { background: #e9ecef; } /* Arrow icon */ .accordion-header::after { content: "▼"; float: right; transition: transform 0.3s ease; } /* Content is hidden by default */ .accordion-content { max-height: 0; overflow: hidden; background: white; padding: 0 1.5rem; transition: all 0.35s ease; } /* When radio is checked → show content */ #item1:checked ~ .accordion-content, #item2:checked ~ .accordion-content, #item3:checked ~ .accordion-content { max-height: 300px; /* ← you need to set a large enough value */ padding: 1.5rem; } /* Rotate arrow when open */ #item1:checked + .accordion-header::after, #item2:checked + .accordion-header::after, #item3:checked + .accordion-header::after { transform: rotate(180deg); } |
Advantages
- No JavaScript → very fast & simple
- Works even with JS disabled
Big disadvantages
- Only one item can be open at a time
- You must set a fixed max-height (bad for dynamic content)
- Harder to make “close all” behavior
So most real projects use JavaScript.
Version 2 – Classic JavaScript Accordion (most common approach)
HTML
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
<div class="accordion"> <div class="accordion-item"> <button class="accordion-header"> What is your return policy? <span class="icon">+</span> </button> <div class="accordion-content"> <p>You can return any item within 30 days of purchase if it's unused and in original packaging.</p> </div> </div> <div class="accordion-item"> <button class="accordion-header"> Do you ship internationally? <span class="icon">+</span> </button> <div class="accordion-content"> <p>Yes, we ship to most countries. Shipping fees are calculated at checkout.</p> </div> </div> <div class="accordion-item"> <button class="accordion-header"> How long does delivery take? <span class="icon">+</span> </button> <div class="accordion-content"> <p>Domestic orders usually arrive in 3–7 business days. International orders take 7–20 days depending on location.</p> </div> </div> </div> |
CSS
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
.accordion { max-width: 720px; margin: 2rem auto; } .accordion-item { border-bottom: 1px solid #ddd; } .accordion-header { width: 100%; padding: 1rem 1.5rem; text-align: left; background: #f9f9f9; border: none; font-size: 1.05rem; font-weight: 600; cursor: pointer; display: flex; justify-content: space-between; align-items: center; transition: background 0.2s; } .accordion-header:hover { background: #f0f0f0; } .accordion-header .icon { font-size: 1.4rem; transition: transform 0.3s; } .accordion-content { max-height: 0; overflow: hidden; padding: 0 1.5rem; background: white; transition: max-height 0.35s ease, padding 0.35s ease; } .accordion-content.show { padding: 1.5rem; } |
JavaScript (clean & modern)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
document.addEventListener("DOMContentLoaded", () => { const headers = document.querySelectorAll(".accordion-header"); headers.forEach(header => { header.addEventListener("click", () => { const content = header.nextElementSibling; const isOpen = content.classList.contains("show"); // Close all other items document.querySelectorAll(".accordion-content.show").forEach(item => { if (item !== content) { item.classList.remove("show"); item.style.maxHeight = null; // Also reset icon item.previousElementSibling.querySelector(".icon").textContent = "+"; } }); // Toggle current item if (isOpen) { content.classList.remove("show"); content.style.maxHeight = null; header.querySelector(".icon").textContent = "+"; } else { content.classList.add("show"); // Set max-height to scrollHeight + some padding content.style.maxHeight = content.scrollHeight + "px"; header.querySelector(".icon").textContent = "−"; } }); }); }); |
Important points:
- We use scrollHeight → dynamic height (no fixed max-height needed)
- We close others automatically (classic accordion behavior)
- Icon changes from + → −
Version 3 – Modern + Accessible + Smooth (2025–2026 style)
Add these improvements:
- Use <details> + <summary> (native HTML element!)
- Add role, aria-expanded, aria-controls
- Better animation with grid or height: auto + transition
HTML (recommended modern way)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<details class="accordion-item"> <summary class="accordion-header"> What is your return policy? </summary> <div class="accordion-content"> <p>You can return any item within 30 days of purchase if it's unused and in original packaging.</p> </div> </details> <details class="accordion-item"> <summary class="accordion-header"> Do you ship internationally? </summary> <div class="accordion-content"> <p>Yes, we ship to most countries. Shipping fees are calculated at checkout.</p> </div> </details> |
CSS
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
.accordion-item { border-bottom: 1px solid #e5e5e5; margin-bottom: 0; } .accordion-header { width: 100%; padding: 1.1rem 1.5rem; background: #fafafa; border: none; text-align: left; font-weight: 600; cursor: pointer; list-style: none; /* removes default marker */ position: relative; transition: background 0.2s; } .accordion-header:hover, .accordion-header:focus { background: #f0f4ff; outline: none; } .accordion-header::-webkit-details-marker { display: none; /* hide default arrow in Chrome/Safari */ } .accordion-header::after { content: "▼"; position: absolute; right: 1.5rem; transition: transform 0.3s; } .accordion-item[open] .accordion-header::after { transform: rotate(180deg); } .accordion-content { padding: 0 1.5rem; overflow: hidden; transition: padding 0.4s ease; } .accordion-item[open] .accordion-content { padding: 1.5rem; } |
Advantages of <details> + <summary>
- Native HTML → no JavaScript needed for basic toggle
- Automatically accessible
- Works with keyboard (Enter/Space)
- Screen readers understand it
Want multiple open at once? Just use <div> instead of <details> and control with JS.
Quick summary – which method to choose?
| Goal | Best method | Needs JS? | Multiple open? | Accessibility | Dynamic height? |
|---|---|---|---|---|---|
| Very simple, no JS | Pure CSS (radio/checkbox) | No | No | Medium | No |
| Classic one-open-at-a-time | JS + buttons | Yes | No | Good | Yes |
| Modern, accessible, minimal JS | <details> + <summary> | No | Yes (default) | Excellent | Yes |
| Full control + animations | JS with scrollHeight + classes | Yes | Optional | Very good | Yes |
Your practice tasks
- Make an accordion where multiple sections can stay open
- Add a smooth height transition without fixed max-height
- Add + / − icons that rotate instead of change character
- Make the header change color when open
- Create nested accordions (accordion inside accordion)
Would you like me to show you any of these next steps with complete code?
Or would you like:
- How to make it look like popular sites (Tailwind version, Material Design, etc.)
- How to animate with CSS grid or height: auto
- How to make it fully keyboard accessible
- Common beginner mistakes
- How to do it in React / Vue / plain JS frameworks
Just tell me what you want to go deeper into — I’ll explain slowly with examples. 😊
