Shopify Section Schema and Blocks: A Developer Reference

A reference for the Shopify section schema: settings, blocks, presets, and limits — with the exact JSON and how each field maps to the theme editor.

6 min readUpdated

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. richtext returns HTML, so output it directly, not through additional filters.
  • image_picker — returns an image object; render with the image_tag or image_url filters.
  • 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 no id.

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:

  • type is required and arbitrary — it's how you tell block types apart in the loop ({% if block.type == 'question' %}).
  • max_blocks caps how many blocks a merchant can add (default and maximum is 50). limit on an individual block type caps that one type.
  • "type": "@app" in a section's blocks array 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.
  • id values must be unique within a settings array and stable over time — renaming an id orphans 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 default later doesn't update sections merchants have already placed.
  • richtext and html outputs are already HTML — don't wrap them in escape or strip_html unless 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.