Two-Column FAQ — Free HTML CSS JS Accordion Snippet
Two-Column FAQ · Layouts · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Two-Column FAQ — Responsive Accordion Grid with grid-template-rows Animation

A long single-column FAQ pushes important questions far down the page; a two-column layout fits twice as many above the fold and reads faster. This component is a responsive two-column FAQ where each question is an independently expandable accordion item, the columns collapse to one on mobile, and the open/close animation uses the modern grid-template-rows technique that animates to a content's natural height without measuring it in JavaScript. It is built in semantic HTML, CSS, and a small data-driven script.
The two-column grid that collapses
The FAQ items live in a CSS grid with grid-template-columns: 1fr 1fr and align-items: start, which is the important detail: start alignment lets each item size to its own content height rather than stretching to match its row neighbour, so an expanded question on the left does not drag an empty gap onto the collapsed one on the right. A single media query at 620px switches the grid to one column, so on phones the FAQ becomes a familiar vertical stack. Each item is its own card with a border that brightens and lifts with a shadow when open.
Animating height with grid-template-rows
The hard part of any accordion is animating to "auto" height — you cannot CSS-transition to height: auto. The classic workarounds are measuring scrollHeight in JavaScript or animating max-height to a guessed value (which makes the timing wrong for short or long answers). This component uses the modern fix instead: the answer is a grid container set to grid-template-rows: 0fr, and opening transitions it to 1fr. An inner wrapper with overflow: hidden clips the content as the row grows. Because 0fr to 1fr is an animatable transition, the answer smoothly expands to exactly its natural height — no measurement, no magic numbers, correct for any answer length.
The rotating plus-to-cross icon
Each question shows a plus icon that rotates 45 degrees into a cross when its item opens, driven purely by transform: rotate(45deg) on the .open state with a CSS transition. Using a single plus that rotates — rather than swapping a plus SVG for a minus SVG — gives a continuous, satisfying motion and means there is no icon-swap flicker. The whole question row is a real <button>, so it is keyboard-focusable and operable with Enter or Space out of the box.
Independent accordion items
Unlike a single-open accordion, every item here toggles independently — opening one does not close the others, which suits an FAQ where a visitor may want several answers open at once for comparison. The click handler simply toggles the .open class on that item and flips its aria-expanded. If you prefer single-open behaviour (only one answer visible at a time), the FAQ section explains the one-line change.
Accessible disclosure wiring
Each question button has aria-expanded reflecting its state and aria-controls pointing at its answer's id; the answer region has role="region" and aria-labelledby pointing back at the question. This is the WAI-ARIA disclosure pattern, so screen readers announce each question as an expandable control and associate the revealed text with the question that owns it. The ids are generated per item in the render loop, so they stay unique no matter how many questions you add.
Customisation
The FAQ is driven by a FAQS array of { q, a } objects — add, remove, or reorder questions there and the grid rebuilds. Items fill the two columns in source order (left, right, left, right…), so order your most important questions first. Swap the #6366f1 accent used by the eyebrow, icon, and open-state border, change the 0.3s expand duration, and edit the header eyebrow, title, and subtitle to match your section. To add a JSON-LD FAQ schema for SEO, mirror the same FAQS array into a @type: FAQPage script — the data is already in the right shape.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to guess why the two-column layout doesn't leave awkward gaps. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why align-items start on the grid is necessary to keep an expanded item on one column from stretching its row-neighbor in the other column, and how the grid-template-rows zero-fraction-to-one-fraction transition avoids the classic "can't animate to height auto" problem. The same assistant can help optimize it — ask whether the current independent-toggle behavior (every item opens on its own) should become exclusive single-open for a very long FAQ list, and what the one-line change to the click handler would look like. It's also useful for extending the section: ask it to generate a matching JSON-LD FAQPage schema block directly from the same FAQS array for rich search results, add category tabs above the grid to filter which questions show, or add deep-linking so a URL hash can open a specific question on page load. 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 responsive two-column FAQ accordion in plain HTML, CSS, and JavaScript with independently-toggling items and full ARIA wiring — no library.
Requirements:
- Render every question-and-answer pair from a single data array of objects, generating a unique id per item in the render loop for use in ARIA attributes — do not hardcode the markup for each question.
- Lay the items out in a CSS grid with two equal columns and align-items set to start (not stretch), so an expanded item in one column never forces its row-neighbor in the other column to grow to match its height; collapse to a single column below a defined mobile breakpoint.
- Animate each answer's expand/collapse using a CSS grid-template-rows transition between a zero-fraction and a one-fraction row (with an inner overflow-hidden wrapper) rather than any JavaScript height measurement or a fixed max-height value, so it works correctly for answers of any length.
- Use a single plus-shaped icon per question that rotates 45 degrees into an X via a CSS transform transition when its item is open, instead of swapping between two different icons.
- Make every item toggle independently of the others (opening one must not close any other item), and wire full disclosure ARIA semantics: each question button needs aria-expanded reflecting its state and aria-controls pointing at its answer's id, and each answer region needs role region and aria-labelledby pointing back at its question's id.
- Explain, in a comment or to the user, how the same underlying data array could be mirrored into a JSON-LD FAQPage schema block for search engine rich results without duplicating the question and answer text.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste the HTML, CSS, and JSA two-column FAQ renders with a centered heading and six collapsible question cards.
- 2Click a questionIts card highlights, the plus icon rotates into a cross, and the answer smoothly expands to its full height.
- 3Open several at onceEach item toggles independently, so visitors can keep multiple answers open to compare.
- 4Resize the windowBelow 620px the two columns collapse into a single vertical stack for mobile.
- 5Edit the questionsChange the FAQS array of { q, a } objects — the grid rebuilds and ARIA ids stay unique automatically.
- 6Match your brandSwap the #6366f1 accent and edit the eyebrow, title, and subtitle in the header markup.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The answer is a CSS grid set to grid-template-rows: 0fr, and opening transitions it to 1fr. An inner wrapper with overflow: hidden clips the content while the row grows. Because 0fr→1fr is an animatable value, the browser expands the row to the content's natural height on its own — no scrollHeight read, no guessed max-height. This is the most reliable accordion animation technique in modern browsers.
In the click handler, before toggling the clicked item, loop the other .fq-item elements and remove their .open class (and reset their button's aria-expanded to false). That gives single-open "exclusive" accordion behaviour. Leaving the handler as-is keeps the default independent behaviour where multiple answers can be open together.
That is usually a missing align-items: start on the grid. Without it, grid items stretch to the tallest in their row, so opening one item adds empty space to its neighbour. With align-items: start, each item sizes to its own content and the columns stay tight regardless of which answers are open.
Your FAQS array is already in the right shape. Build a JSON-LD object with @type: FAQPage and a mainEntity array mapping each { q, a } to a Question with an acceptedAnswer, then inject it in a <script type="application/ld+json">. This makes the page eligible for FAQ rich results. Keep the visible answers and the schema text identical, as search engines expect them to match.
Render the FAQS array with .map/v-for/*ngFor and track open state — either a Set of open indices (for independent toggles) or a single openIndex (for exclusive). Bind the .open class and aria-expanded to that state. All the CSS, including the grid-template-rows animation and the icon rotation, ports unchanged; only the toggle state moves into the framework. Generate stable ids from the index for aria-controls / aria-labelledby.