More Navigation Snippets
Accordion FAQ — Free HTML CSS JS Snippet
Accordion / FAQ · Navigation · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Accordion FAQ — Single-Open Toggle, max-height Transition & Chevron Rotation

An accordion FAQ lets users expand one question at a time to read the answer, keeping the page compact while making all questions visible. The single-open pattern — collapsing the previous item when a new one opens — is the standard for FAQs, settings panels, and any list of collapsible content.
The toggle function
toggle(btn) finds the .item via btn.closest('.item'), checks if it has .open, then closes ALL open items with querySelectorAll('.item.open').forEach(i => i.classList.remove('open')). If the clicked item was not already open, it adds .open. This single-open-at-a-time pattern prevents the FAQ from becoming an incomprehensible wall of open answers.
The max-height CSS transition
The .answer div has max-height: 0; overflow: hidden; transition: max-height 0.3s by default. When .open is added, max-height: 500px applies. CSS animates between these values. Using max-height instead of height works because height: auto cannot be transitioned — max-height on a value large enough to never clip the content effectively animates the reveal.
The chevron rotation
The question row contains an SVG chevron. .item.open .q svg { transform: rotate(180deg) } with transition: transform 0.3s rotates the chevron when the item opens. This communicates the expand/collapse state at a glance.
Initial open state
The last dispatchEvent(new MouseEvent('click')) call in the JS opens the first item on page load. Remove this line to start with all items closed.
The max-height animation technique
CSS cannot animate from height: auto to height: 0 directly. The accordion uses max-height instead: the closed state has max-height: 0; overflow: hidden. The open state applies max-height: 500px (larger than any possible content). CSS transition: max-height 0.3s ease animates between these values. The transition feels natural even though max-height: 500px is much larger than the actual content height — the ease-out timing makes it decelerate near the end, which coincidentally matches when the content is near its natural height.
Chevron rotation
The chevron uses transform: rotate(180deg) when .open is applied to the parent item. CSS transition: transform 0.25s eases the rotation. This provides visual feedback that the item is a toggle without requiring a separate open/close icon pair.
Single-open accordion behaviour
The toggle() function closes all items before opening the clicked one: document.querySelectorAll('.faq-item').forEach(el => el.classList.remove('open')). This ensures only one answer is visible at a time — the accordion pattern. Remove this close-all block to allow multiple items open simultaneously if your FAQ content benefits from side-by-side comparison.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to puzzle out the timing tricks alone — paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why max-height is used instead of height for the answer transition, and why the value is set to a fixed number like 200px rather than something dynamic. The same assistant can help optimize it — for instance asking whether a ResizeObserver-based measurement would be more robust than a hardcoded max-height for answers of very different lengths. It's just as useful for extending the component: ask it to add aria-expanded and aria-controls for full accessibility, animate multiple items open at once by removing the close-all loop, or wire it up to FAQ schema JSON-LD automatically from the same data. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a single-open accordion FAQ in plain HTML, CSS, and JavaScript — no libraries, using only class toggling and a max-height transition for the expand/collapse animation.
Requirements:
- A list of question/answer items, each with a button header (containing the question text and a chevron SVG icon) and a hidden answer panel below it.
- Clicking a question's button must first remove an "open" class from every other item, then toggle it onto the clicked item only if it wasn't already open — so at most one answer is expanded at any time, and clicking an already-open item closes it.
- The answer panel must start with max-height: 0 and overflow: hidden, and transition to a max-height value large enough to never clip the tallest expected answer when the "open" class is present — explain in a comment why height: auto cannot be transitioned and max-height is the workaround.
- The chevron icon must rotate 180 degrees with its own CSS transition when its parent item gains the "open" class, so it visually points the opposite direction when expanded.
- One item should be open by default when the page loads (not all closed).
- Do not use any animation or accordion library — the entire interaction should be driven by one small toggle function that manipulates classList.Step by step
How to Use
- 1Click each questionClick question headers to expand answers. Clicking a new question collapses the current open item — single-open pattern.
- 2Update questions and answersIn the HTML panel, change the .q button text and .a div content for each .item.
- 3Add more itemsCopy an .item div and paste it inside .faq. The toggle function picks up any .item with a .q button.
- 4Change animation speedUpdate 0.3s on transition: max-height in the CSS panel.
- 5Allow multiple open itemsIn the JS panel, remove the querySelectorAll close-all loop to allow multiple items to be open simultaneously.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
toggle() first closes all elements with .open via querySelectorAll(".item.open").forEach(i => i.classList.remove("open")). Then if the clicked item was not already open, it adds .open. Clicking an already-open item closes it without reopening.
height: auto cannot be CSS-transitioned. max-height on a value larger than the content (500px) acts as height: auto — the element shows its full height. Setting max-height: 0 with overflow: hidden collapses it. CSS transitions can animate between these two max-height values.
Remove the querySelectorAll(".item.open").forEach(i => i.classList.remove("open")) loop from toggle(). Only the final if (!isOpen) item.classList.add("open") needs to remain.
Add a <script type="application/ld+json"> with @type: FAQPage and mainEntity array. Each item in the array has @type: Question, name (the question text), and acceptedAnswer.text (the answer). Google uses this for rich results in search.
Add aria-expanded="false" to the .q button and toggle it to "true" when .open is added. Add aria-controls="answer-id" pointing to the .answer div id. Add role="region" and aria-labelledby to the answer container.
Yes. Click "JSX" for a React component. In React, manage an openIndex state. Compare each item index to openIndex to determine the .open class and chevron rotation. Set openIndex to null or to the clicked index.