Part of our guide to Shopify Custom Sections: The Complete Guide.
An FAQ section answers the questions that otherwise land in your support inbox, and the best version lets a merchant add, edit, and reorder questions from the theme editor without touching code. This guide builds exactly that: a collapsible FAQ accordion using section blocks, so each question-and-answer pair is an item a merchant manages in the editor — plus the correct way to add FAQ structured data for search.
This is part of the Shopify custom sections guide.
If a .liquid section file and its {% schema %} are new to you, start with
How to create a custom section.
Why blocks, not settings
You could add ten text settings for ten questions, but that caps the section at
ten and forces a fixed order. Blocks solve both problems: a block is a
repeatable item inside a section that a merchant can add, remove, and drag to
reorder. Each FAQ row becomes one block with a question and an answer field.
Blocks are covered in depth in
Section schema and blocks;
here we use them for a concrete FAQ.
Step 1 — Build the section with FAQ blocks
Create sections/faq.liquid. The markup loops over section.blocks and renders
each as a native <details>/<summary> disclosure — which gives you a working
accordion with zero JavaScript and full keyboard and screen-reader support out of
the box.
<section class="faq">
{% if section.settings.heading != blank %}
<h2 class="faq__heading">{{ section.settings.heading }}</h2>
{% endif %}
<div class="faq__list">
{% for block in section.blocks %}
<details class="faq__item" {{ block.shopify_attributes }}>
<summary class="faq__question">{{ block.settings.question }}</summary>
<div class="faq__answer">{{ block.settings.answer }}</div>
</details>
{% endfor %}
</div>
</section>
{{ block.shopify_attributes }} is important: it lets the theme editor highlight
and select the exact block a merchant clicks on, so editing feels native. Never
omit it on a block's root element.
Step 2 — Define the schema and blocks
Add the schema at the bottom of the same file. The blocks array declares a
faq_item block type with its two settings; max_blocks caps the count if you
want to, and the presets entry can seed a couple of starter questions so the
section is not empty the moment it is added.
{% schema %}
{
"name": "FAQ",
"tag": "section",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Frequently asked questions"
}
],
"blocks": [
{
"type": "faq_item",
"name": "Question",
"settings": [
{
"type": "text",
"id": "question",
"label": "Question",
"default": "How long does shipping take?"
},
{
"type": "richtext",
"id": "answer",
"label": "Answer",
"default": "<p>Orders ship within 2 business days.</p>"
}
]
}
],
"max_blocks": 25,
"presets": [
{
"name": "FAQ",
"blocks": [
{ "type": "faq_item" },
{ "type": "faq_item" }
]
}
]
}
{% endschema %}
Two notes:
- The
answerusesrichtext, so merchants can bold text and add links inside an answer without HTML. - The block
type(faq_item) is what thepresets.blocksentries reference — the strings must match.
Step 3 — Style the accordion
The <details> element already opens and closes on click. A little CSS makes it
look intentional and rotates a caret when open:
{% stylesheet %}
.faq { max-width: 48rem; margin: 0 auto; padding: 2rem 1.5rem; }
.faq__item { border-bottom: 1px solid #e5e5e5; }
.faq__question {
cursor: pointer;
list-style: none;
padding: 1rem 0;
font-weight: 600;
display: flex;
justify-content: space-between;
}
.faq__question::after { content: "+"; }
.faq__item[open] .faq__question::after { content: "–"; }
.faq__answer { padding: 0 0 1rem; }
{% endstylesheet %}
list-style: none on the summary hides the browser's default triangle so your
+/– marker is the only indicator.
Step 4 — Add FAQ structured data (carefully)
FAQ structured data can make your questions eligible for rich results in search.
Add FAQPage JSON-LD that mirrors the visible questions and answers — Google
requires the marked-up content to match what shoppers actually see on the page.
Loop the same blocks into the schema so the two never drift apart:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{%- for block in section.blocks -%}
{
"@type": "Question",
"name": {{ block.settings.question | json }},
"acceptedAnswer": {
"@type": "Answer",
"text": {{ block.settings.answer | strip_html | json }}
}
}{%- unless forloop.last -%},{%- endunless -%}
{%- endfor -%}
]
}
</script>
The | json filter safely escapes quotes and newlines for you — building JSON by
hand is where most invalid-schema errors come from. Note that Google now limits FAQ
rich results to authoritative government and health sites for most queries, so
treat the schema as correct-but-optional rather than a guaranteed snippet. For
general storefront layout choices, see the
features overview.
Keep the FAQ section update-safe
Built as faq.liquid, this section lives in your theme files. On a theme update or
theme switch, Shopify publishes a fresh copy of those files and your faq.liquid
is not carried over — and because block content is stored per template against a
section that no longer exists, the questions a merchant typed can be lost too. The
full explanation is in
Update your theme without losing customizations.
If your FAQ is a permanent fixture, rendering it from an app via a Theme App Extension means a theme update or swap never removes it. SectionGuard takes this approach — the FAQ renders from the app with the same block-based editing merchants expect, so it survives every update.