Part of our guide to Shopify Custom Sections: The Complete Guide.
The {% schema %} block is the contract between your section and the theme editor.
It's a single JSON object that declares the section's name, its editable settings,
its repeatable blocks, and how it appears in the "Add section" menu. Get it
right and merchants configure everything without touching code; get a comma wrong
and the section refuses to load. This is a reference for the parts you reach for
most, with the exact JSON for each.
It pairs with how to create a custom section, which walks through building one end to end — this page is the field-by-field detail behind that.
The shape of a schema
The schema lives at the bottom of a section's .liquid file, wrapped in
{% schema %} … {% endschema %}. It must be valid JSON — no trailing commas, no
comments, no Liquid inside it. The top-level keys you'll use most:
{% schema %}
{
"name": "Feature banner",
"tag": "section",
"class": "feature-banner-section",
"settings": [],
"blocks": [],
"max_blocks": 12,
"presets": [{ "name": "Feature banner" }]
}
{% endschema %}
name— the label shown in the editor.tag/class— the HTML element Shopify wraps the section in, and an extra class on that wrapper. Handy for scoping styles.settings— section-level fields (see below).blocks/max_blocks— repeatable child items and their cap.presets— makes the section addable from the editor.
Settings: the editable fields
Each entry in settings becomes one field in the editor. Every input needs a
type, an id (what you read as section.settings.<id> in Liquid), and a
label. Most take an optional default.
"settings": [
{ "type": "text", "id": "heading", "label": "Heading", "default": "Sale" },
{ "type": "richtext", "id": "body", "label": "Body text" },
{ "type": "image_picker", "id": "image", "label": "Image" },
{ "type": "url", "id": "link", "label": "Button link" },
{ "type": "checkbox", "id": "full_width", "label": "Full width", "default": false },
{
"type": "select", "id": "align", "label": "Alignment", "default": "center",
"options": [
{ "value": "left", "label": "Left" },
{ "value": "center", "label": "Center" }
]
},
{ "type": "color_scheme", "id": "scheme", "label": "Color scheme" }
]
Common input types worth knowing:
text,textarea,richtext— plain, multi-line, and formatted text.richtextreturns HTML, so output it directly, not through additional filters.image_picker— returns an image object; render with theimage_tagorimage_urlfilters.url,collection,product,blog,page— resource pickers that return the linked object or its handle.select,radio,checkbox,range— choice and numeric inputs.color,color_scheme— a single color, or a full theme color scheme you can apply to the section wrapper.header,paragraph— presentational; they add labels and help text in the editor and have noid.
You read every one of these the same way in the markup:
<h2>{{ section.settings.heading }}</h2>
{% if section.settings.image %}{{ section.settings.image | image_url: width: 800 | image_tag }}{% endif %}
Blocks: repeatable child items
Blocks are what let a merchant add, remove, and reorder items inside a section —
slides in a slider, rows in an FAQ, logos in a marquee. Each block type has its own
settings, and you loop over section.blocks to render them.
{% schema %}
{
"name": "FAQ",
"blocks": [
{
"type": "question",
"name": "Question",
"settings": [
{ "type": "text", "id": "q", "label": "Question" },
{ "type": "richtext", "id": "a", "label": "Answer" }
]
}
],
"max_blocks": 25,
"presets": [
{
"name": "FAQ",
"blocks": [{ "type": "question" }, { "type": "question" }]
}
]
}
{% endschema %}
Render them by looping, and always emit {{ block.shopify_attributes }} on each
block's wrapper — it's what makes a block clickable and highlightable in the theme
editor:
<div class="faq">
{% for block in section.blocks %}
<details class="faq__item" {{ block.shopify_attributes }}>
<summary>{{ block.settings.q }}</summary>
<div>{{ block.settings.a }}</div>
</details>
{% endfor %}
</div>
A few block details that trip people up:
typeis required and arbitrary — it's how you tell block types apart in the loop ({% if block.type == 'question' %}).max_blockscaps how many blocks a merchant can add (default and maximum is 50).limiton an individual block type caps that one type."type": "@app"in a section'sblocksarray lets app blocks be inserted into your section — that's the hook Theme App Extensions use.
Presets: appearing in the editor
Without a presets entry, a section exists but can't be added from the editor —
it can only be referenced from a template's JSON. presets also lets you ship
default settings and default blocks, so a merchant who adds the section gets a
sensible starting point rather than an empty shell.
"presets": [
{
"name": "Feature banner",
"settings": { "heading": "Free shipping over $50" },
"blocks": [{ "type": "question" }]
}
]
Limits and gotchas
A short list of the constraints that cause the most "why won't it load" moments:
- The schema must be strict JSON. No trailing commas, no comments, no single
quotes, and no Liquid tags or
{{ }}inside the schema. A single syntax error makes the whole section fail to render. idvalues must be unique within a settings array and stable over time — renaming anidorphans any value a merchant already saved under the old one.- 50 settings / 50 blocks is the practical ceiling per section; a section that needs more usually wants to be split.
- Defaults only apply on insertion. Changing a
defaultlater doesn't update sections merchants have already placed. richtextandhtmloutputs are already HTML — don't wrap them inescapeorstrip_htmlunless you mean to.
For applying merchant-set colors from the schema to your markup, see how to add custom CSS to a section; for reading store data through settings and metafields, see custom Liquid and JavaScript, safely.
Where the schema lives — and why that matters
A schema is part of a section file in your theme. That means the settings a merchant configures against it are stored per template, and both the section and those saved values are tied to the current theme version. When you update or switch themes, Shopify publishes a fresh copy of the theme's files; a section file you added by hand isn't carried over, and settings that referenced it can be lost with it. The workflow for surviving that is in update your theme without losing customizations.
The structural alternative is to render the section — schema, settings, and all — from an app via a Theme App Extension, so no theme update touches it. SectionGuard does exactly that: the same schema-driven settings merchants expect in the editor, rendered from the app so the section survives every theme update and swap. See how the two models differ on our comparison.