Production Shopify · FAQ sections
Build maintainable FAQ sections for Shopify
Static accordion HTML is easy to paste once and expensive to own. This guide is about FAQ modules that stay editable, accessible, searchable, and safe when merchants add the twentieth question six months after launch.
Production implementation guide · Updated 2026-08-03
What “maintainable” means for a Shopify FAQ
A maintainable FAQ is not “it opens and closes on the homepage.” It is a section the content team can extend without a developer, that remains accessible on keyboard and screen readers, that does not break when duplicated on a product template, and that still answers search intent when JavaScript is slow or disabled.
Maintainable traits
- Each question/answer is a block (or clearly typed rows)—not hardcoded HTML in the section file
- Merchants add, remove, and reorder rows in the theme editor without deploying
- Open/close state is exposed to assistive tech (buttons + aria-expanded, not clickable divs)
- Answers remain in the DOM for find-in-page and crawlers—not trapped only inside closed shadow tricks
- Multiple FAQ sections on one page isolate their JavaScript by section.id
Unmaintainable patterns
- Six questions baked into Liquid “temporarily” that become permanent
- jQuery accordion targeting .faq-item globally across the theme
- Answers loaded only after click via unreachable empty containers
- Setting labels like q1 / a1 that nobody on the brand team understands
Choose blocks when questions will grow
Flat section.settings work for a fixed three-item promo FAQ that will never change shape. Almost every real store FAQ grows. Blocks are the maintainable default: one block type per row, settings for question and answer, optional category if you truly need filters later.
Do not invent ten parallel settings (question_1…question_10). That pattern expires the day they need eleven.
Blocks schema — one row type merchants understand
{
"name": "FAQ",
"blocks": [
{
"type": "question",
"name": "Question",
"settings": [
{
"type": "text",
"id": "title",
"label": "Question",
"default": "How long does shipping take?"
},
{
"type": "richtext",
"id": "answer",
"label": "Answer",
"default": "<p>Standard shipping arrives in 3–5 business days.</p>"
}
]
}
],
"presets": [
{
"name": "FAQ",
"blocks": [
{
"type": "question",
"settings": {
"title": "How long does shipping take?",
"answer": "<p>Standard shipping arrives in 3–5 business days.</p>"
}
},
{
"type": "question",
"settings": {
"title": "What is your return policy?",
"answer": "<p>Unworn items can be returned within 30 days.</p>"
}
}
]
}
]
}Seeded presets teach repetition. An empty block list looks broken and invites hardcoding.
Accordion markup that stays accessible
Maintainable FAQs fail quietly when interaction is a clickable <div>. Use a <button> for the question control, bind aria-expanded, and point aria-controls at the answer panel id. Keep the answer in the document—hide visually if needed, but do not rely on removing content from the accessibility tree in inconsistent ways.
Heading hierarchy matters: a section title can be an h2; each question is often best as the button text, not a fake heading inside a button unless you have a deliberate outline strategy.
Button + aria-expanded panel pattern
<div class="faq" data-faq-root>
{% for block in section.blocks %}
{% assign panel_id = 'FaqAnswer-' | append: section.id | append: '-' | append: block.id %}
<div class="faq__item" {{ block.shopify_attributes }}>
<button
type="button"
class="faq__question"
id="FaqQuestion-{{ section.id }}-{{ block.id }}"
aria-expanded="false"
aria-controls="{{ panel_id }}"
data-faq-trigger
>
{{ block.settings.title | escape }}
</button>
<div
class="faq__answer rte"
id="{{ panel_id }}"
role="region"
aria-labelledby="FaqQuestion-{{ section.id }}-{{ block.id }}"
hidden
data-faq-panel
>
{{ block.settings.answer }}
</div>
</div>
{% endfor %}
</div>escape the question text. Keep answer as richtext through .rte. Unique ids must include section.id and block.id so duplicates never collide.
Theme editor workflow merchants must survive alone
shopify_attributes on each item root is what makes block selection highlight the right row. Without it, merchants edit the wrong question and publish incorrect policy text.
Presets must appear under Add section with a plain name (“FAQ”). After insert, reordering blocks should change storefront order immediately—that is the maintainability demo.
- Add section lists “FAQ” (or your preset name) without developer jargon.
- Sidebar shows each question as its own block; selecting one highlights that row.
- Dragging block order updates the storefront sequence after save.
- Clearing an answer richtext does not leave an empty open panel chrome.
If any checkpoint fails, the FAQ will generate support tickets after handoff.
Search behaviour: answers people (and crawlers) can find
FAQs exist to answer questions. If answers are empty shells until JavaScript runs, find-in-page fails and some crawlers see less than shoppers do. Prefer rendering answer HTML in the page and controlling visibility with hidden/CSS, not replacing innerHTML after fetch.
For site search and SEO strategy, consistent question wording matters more than animation. Match the language customers type in support (“return unworn items”) rather than marketing slogans inside the question field.
Optional FAQPage JSON-LD from the same blocks
{% if section.blocks.size > 0 %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{% for block in section.blocks %}
{
"@type": "Question",
"name": {{ block.settings.title | json }},
"acceptedAnswer": {
"@type": "Answer",
"text": {{ block.settings.answer | strip_html | json }}
}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
}
</script>
{% endif %}Only emit FAQPage when this section is the canonical FAQ for the URL. Duplicate FAQPage graphs on every template create junk structured data—gate carefully.
Accordion JavaScript that survives multiple FAQs
Bind listeners inside the section root. Query triggers relative to that root. Toggle only the controlled panel. Never document.querySelectorAll('.faq__question') at document scope on a theme that might host three FAQs.
Section-scoped toggle sketch
(function () {
const root = document.querySelector('[data-faq-root]');
if (!root) return;
root.querySelectorAll('[data-faq-trigger]').forEach((button) => {
button.addEventListener('click', () => {
const expanded = button.getAttribute('aria-expanded') === 'true';
const panelId = button.getAttribute('aria-controls');
const panel = root.querySelector('#' + CSS.escape(panelId));
if (!panel) return;
button.setAttribute('aria-expanded', String(!expanded));
panel.hidden = expanded;
});
});
})();In production, prefer a small asset loaded once per section instance or an inline script that receives section.id. The rule is isolation—not the exact bundling choice.
Production debugging checklist for FAQ sections
When an FAQ “stops working,” separate content problems from interaction problems from editor problems. Most emergencies are one of: missing blocks, JS isolation failure, or schema/preset discoverability—not Liquid syntax.
| Symptom | Checks |
|---|---|
| New questions do not appear | Block added? Section saved? Correct template? App or cache layer? |
| Click does nothing | Console errors? Root selector match? Duplicate ids? Script loaded on that template? |
| Wrong row highlights in editor | shopify_attributes on item root? Nested wrappers stealing attributes? |
| Answers missing for SEO/find | Answer rendered server-side? hidden vs empty node? App replacing content? |
Practice building the FAQ by hand
Implement this on a development theme before using any generator. You should be able to explain every aria attribute and every block setting.
- 01
Create sections/faq.liquid
Blocks schema with two seeded questions, button/panel markup, shopify_attributes, namespaced ids.
- 02
Wire accessible toggles
Section-scoped script updating aria-expanded and panel hidden state.
- 03
Register and insert
Add to a JSON template, then also insert via Add section on a second template.
- 04
Duplicate stress test
Place two FAQ sections on one page; confirm isolation and unique ids.
- 05
Content ops rehearsal
Have someone else add a question, reorder, and clear an answer without your help.
Use the converter only after the FAQ model is clear
If you already have static accordion HTML, the converter can draft Liquid faster. You still must enforce blocks, accessible controls, section-scoped behaviour, and editor-ready presets before calling the FAQ maintainable.
Supporting draft acceleration — not the definition of a finished FAQ section.
What to study next for FAQ-quality sections
Paths that deepen blocks, schema, and accessible storefront UI—not keyword landings.
Beginner
Liquid and section basics before accordion behaviour.
- Shopify Liquid for beginners
Read block loops and filters without guessing.
- How Shopify sections work
Where FAQ sections sit in OS 2.0 templates.
Intermediate
Schema and blocks craft for growing Q&A lists.
- Dynamic Shopify blocks guide
Reorderable rows beyond a single FAQ.
- Shopify schema guide
Setting types, presets, and editor UX.
- Shopify section tutorial
File-to-template shipping workflow.
Advanced
Delivery under policy and support pressure.
- Shopify development experience
How teams handle content emergencies on live themes.
- Accordion patterns (related UI)
When FAQ UX overlaps general accordion modules—keep concerns separate.
- Resources hub
Curriculum index for continued study.
Editorial review
Reviewed by the HTML to Liquid Converter team


Content is reviewed by the HTML to Liquid Converter Shopify development team before publication. Technical accuracy is validated against current Shopify Online Store 2.0 conventions and active client theme work.
Last updated:
Questions or corrections? Contact us.