FAQ accordion
A list of questions and answers, written as plain markup: one block per question, a heading and whatever the answer holds, three questions or thirty.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialThe look of the preview is thefaq-*classes of the paste. The Embed holds what moves, and reads a few variables,--faq-ink,--faq-gutter(the column of the signs, and where the answers start),--faq-pad(the space above and below a question),--faq-markand--faq-stroke(the plus): set them on the list, or on anything above it, in a rule of your own. The space under an open answer is the bottom padding of the answer's class. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Use it on your own elements
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the element on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-faq Marks the list of questions. It takes no value: leave the value field empty. The settings below go on the same element. data-faq-item On the block that holds one question and its answer. It takes no value. Repeat it for each question, three or thirty. Its id is what the address carries: page#delivery opens that question. In Webflow the id is the ID field of the Settings panel. Without an id the engine writes one from the words of the question. [ Required ] data-faq-question On the question: a heading that holds the words and nothing else. It takes no value. The engine puts a real button in it, with the sign before the words. An item without one is left alone and the engine says so in the console. [ Required ] data-faq-answer On the block that holds the answer. It takes no value. It holds anything, one line or three paragraphs, links and lists, and its height is never measured. [ Required ] data-faq-open A state, not a mark to write: the engine puts it on an open item, and takes it off. The button carries aria-expanded, an item on its way data-faq-moving, a list with an answer open data-faq-any, a mounted list data-faq-ready. Never written by hand; the stylesheet reads them, and so can a rule of yours. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same element. An attribute always wins over the defaults written in the code.Attribute Default What it is data-faq-mode single How many answers may be open at once. Options: single, opening a question closes the one that was open, and the open one can still be closed; multiple, each question opens and closes on its own, the better choice when readers compare answers. data-faq-start 1 The question that is open when the page arrives, when the address names none. Options: a number counted from 1, 2 for the second question; 0 for none; the id of an item, delivery; all, with mode at multiple. data-faq-hash on Whether the open question is in the address of the page. Options: on, a question opened by the visitor writes the id of its item after the # without adding a step to the history, and an address or a link that names an item, or anything inside one, opens it; off, the address is neither read nor written. data-faq-schema off Whether the engine adds FAQPage structured data to the head of the page, built from the questions and answers. Options: off, nothing is added; on, one JSON-LD script, see "Structured data" for what it is worth today. data-faq-duration 1 The time an answer takes to open or to close, in seconds.
Two files, no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
FaqAccordion.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-faq-mode single How many answers may be open at once. Options: single, opening a question closes the one that was open, and the open one can still be closed; multiple, each question opens and closes on its own, the better choice when readers compare answers. data-faq-start 1 The question that is open when the page arrives, when the address names none. Options: a number counted from 1, 2 for the second question; 0 for none; the id of an item, delivery; all, with mode at multiple. data-faq-hash on Whether the open question is in the address of the page. Options: on, a question opened by the visitor writes the id of its item after the # without adding a step to the history, and an address or a link that names an item, or anything inside one, opens it; off, the address is neither read nor written. data-faq-schema off Whether the engine adds FAQPage structured data to the head of the page, built from the questions and answers. Options: off, nothing is added; on, one JSON-LD script, see "Structured data" for what it is worth today. data-faq-duration 1 The time an answer takes to open or to close, in seconds. Snappier; under 0.6 the lines are hardly seen rising one by one; 0 opens at once Slower and more shown; over 1.4 the list feels late on the press -
Drive it from JavaScript
When a script opens a question, follows which ones are open, or needs the structured data.const element = document.querySelector('[data-faq]'); element.addEventListener('faq:settled', (e) => { console.log(e.detail); }); // Follow what is opened: an analytics call, for one. element.addEventListener('faq:change', (event) => { console.log(event.detail.item.id, event.detail.open, event.detail.by); }); const faq = FaqAccordion.mount(element); // the list auto() mounted, or a new one faq.open('delivery'); // by the id of an item, or by a number counted from 0 faq.closeAll(); console.log(faq.schema()); // the FAQPage JSON-LD, to paste in the head // When the list leaves the page: FaqAccordion.destroy(element); // removes what the engine added, gives the markup back as it was
-
Fit it to your page
Questions and answers
- One block per question. A block marked
data-faq-itemholds a heading markeddata-faq-questionand a block markeddata-faq-answer. To add a question, copy a block; to remove one, delete it; to reorder, move it. In Webflow, duplicate an item in the Navigator, or bind one item to a Collection list. - Write the question as plain words in a heading, at the level that fits the page (
h3under anh2). Do not write a button or a sign: the engine puts a real<button>inside the heading, with the sign before the words, and takes them out ondestroy(). - An answer holds anything: one line, three paragraphs, links, lists, a picture. Its height is never measured, so an answer may change while it is open.
- **Give an item an
id** in the page's own words (delivery, notfaq-2) when you want a short address. Without one the engine writes anidfrom the words of the question,how-do-we-pay. - Several lists on a page each work on their own.
The keyboard
Tab goes from question to question, and into an open answer when it holds a link. Enter or Space opens and closes the question. With the focus on a question, Down and Up go to the next and the previous one round the ends, Home and End to the first and the last.
The address
With
hashaton, the default:- a question opened by the visitor writes
#and theidof its item in the address, withhistory.replaceState: the Back button still leaves the page in one step; closing it takes the#off; - a page that arrives with a hash naming an item, or anything inside an answer, opens that question and scrolls to it;
- a link on the same page to such an
iddoes the same.
A question opened by a script writes nothing.
The browser's own search
A closed answer carries
hidden="until-found"where the browser knows it (Chrome and Edge since 102, Firefox since 139): Ctrl+F finds a word inside a closed answer, and the browser opens it, which the engine follows. A link to a piece of text (#:~:text=) does the same. Safari has not shipped it at the time of writing (October 2026): there a closed answer is plainly hidden, and its search does not look inside. In every browser a closed answer is out of the tab order and out of the accessibility tree.Structured data
data-faq-schema="on"adds one<script type="application/ld+json">to the head: aFAQPagewith every question of the list and its answer, the answer keeping its paragraphs, lists and links and nothing else. What it is worth, honestly:- Google no longer shows FAQ rich results, the unfolding questions under a search result: limited to government and health sites in August 2023, then ended for every site on 7 May 2026. Google says it still reads the markup to understand the page, and that leaving it in place causes no error. It will not change how the result looks.
- **Other readers still use
FAQPage**: Bing, and the assistants that answer from pages. Most of them do not run JavaScript, so a script added by the engine is not seen by them. For those, callFaqAccordion.mount(element).schema()once in the console, and paste the JSON it returns in a<script type="application/ld+json">of the page's own head, with the setting leftoff. - **One
FAQPageper page.** When the page already carries one, or a second list asks for one, the engine adds nothing and says so.
The colours and the sizes
Custom properties, set at zero specificity on the list. Give them your own values on the list or on anything above it:
--faq-ink: the questions and their signs.--faq-gutter: the width of the column the signs stand in, which is also where the answers start.--faq-pad: the space above and below the words of a question, 12px in the preview. With the line of the question it makes the step from one question to the next.--faq-markand--faq-stroke: the size of the plus and the thickness of its bars.
The type of a question is the heading's own class; the type, the width and the padding of an answer are the answer's own class. The space under an open answer is the answer's bottom padding.
From JavaScript
FaqAccordion.mount(element, overrides)returns the instance of a list, mounting it if it was not:{ element, settings, items(), opened(), isOpen(which), open(which, options), close(which, options), toggle(which, options), closeAll(options), schema(), destroy() }.whichis a number counted from 0, theidof an item, or an element of one;{ instant: true }changes with no motion; each returnsfalsewhen nothing changed.FaqAccordion.auto(root)mounts every marked list insideroot, and runs once on its own;FaqAccordion.destroy(element)gives the markup back.- Events, all bubbling from the list:
faq:ready;faq:changeas soon as a question opens or closes, with{ index, item, open, by }indetail,bybeinguser,hash,findorapi;faq:settledonce everything has stopped, with the numbers of the open questions. - The state is in the markup:
aria-expandedon the button,data-faq-openon an open item,data-faq-movingon an item on its way,data-faq-anyon a list with an answer open,data-faq-readyon the list. - **Under
prefers-reduced-motion: reduce** a question opens at once. - Without JavaScript the page is headings and answers, all of them in sight, in order.
- The words of the demo are the demo's: on a real page every question and every answer is the user's own, or asked for.
- One block per question. A block marked
-
Avoid the pitfalls
- The heading holds words, not a button. A question written as a link or a button ends up as a button inside a button. Plain text, with an emphasis if needed.
- Do not hide the answers yourself, with CSS or with
hidden: the engine does it once it is there, so that a page without JavaScript still shows every answer. - **Do not use
<details>** for the items: it brings its own opening, which cannot be eased the same way in every browser, and its own state. - In Webflow, do not use the native Dropdown or Accordion elements: they bring their own script and classes. The paste is plain blocks.
- The stylesheet draws the states. Without
faq-accordion.cssa closed answer is hidden with no motion and no sign shows, and the engine says so in the console. - A list at the very top of a short page has no scroll to give: when an answer above closes there, the pressed question moves up with it.
- **An
idused twice on a page** breaks the address: the hash opens the first element that carries it. - Questions or answers added after the mount need
destroy()thenmount(). Text changed inside an answer needs nothing, unlessschemaison: the JSON is built once, at the mount. - A list inside an answer of another list is left alone by the outer one and mounts on its own.
- In React or any framework that owns the DOM, mount in an effect and call
destroy()in its cleanup; the engine moves the words of each question into a button and wraps each answer in two boxes.
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
FAQ accordion is a site utility component for Webflow and vanilla JavaScript, with no library. Also called accessible accordion, smooth accordion animation height auto or faq schema accordion.
A list of questions and answers, written as plain markup: one block per question, a heading and whatever the answer holds, three questions or thirty. A plus stands before each question, in a column of its own, and the answer starts under the words; nothing is ruled, an even step from one question to the next does the work. A press opens the answer: the box grows from nothing to the height of the answer with nothing measured, and the lines of the answer, the browser's own lines whatever the answer holds, rise one after the other from behind their own lower edge as the box makes room for them; closing, they go back down from the last to the first. The upright bar of the plus draws in, leaving a minus. The text stays real text throughout, and the markup is untouched once the move is over. One number from 0 to 1 runs all of it, so a second press in the middle of the first turns round as a curve, and any number of quick presses leave no step and no jump. When an answer above closes, the question that was pressed stays where it is on the screen and the page moves round it. One answer open at a time or several, as a setting. The accordion pattern in full: a real button in each heading, aria-expanded, aria-controls, arrows, Home and End. The open question is in the address, so a link opens the right one. A closed answer is still found by the browser's own search, which opens it. Without JavaScript every answer is in the page. Reduced motion respected. Optional FAQPage structured data. No library. Pattern type accordion, FAQ, disclosure, collapsible content.
Updated
Questions
Paste the block: it arrives as a list marked data-faq holding one div per question marked data-faq-item, each with a heading marked data-faq-question and a block marked data-faq-answer. Duplicate an item in the Navigator to add a question; the engine puts the button and the plus sign in each heading on the published page.
Yes. Each question becomes a real button inside its heading, with aria-expanded and aria-controls written by the engine. Tab goes from question to question, Enter or Space opens and closes one, Up, Down, Home and End move between them, and a closed answer is out of the tab order.
The box of an answer is one grid row that goes from 0fr to 1fr, so its height follows the answer with nothing measured, and the lines of the answer rise one after the other as the box makes room. A press in the middle of a move turns round from where it is, and when an answer above closes, the question that was pressed stays where it is on the screen.
Yes to both. A closed answer carries hidden="until-found" in Chrome, Edge and Firefox, so the browser's search finds a word in it and opens it; Safari does not support it yet. Each item has an id that opening writes after the # of the address, and a link to that id opens the question.
It no longer changes how a result looks: Google ended FAQ rich results for every site on 7 May 2026, though it says it still reads the markup. data-faq-schema="on" adds the JSON-LD from the questions of the list, and schema() returns it as text to paste in the head for readers that do not run JavaScript.