Themes
Sections and schema
Sections and schema
A section is a Liquid file in sections/ that a merchant can add, reorder, configure and remove in the theme editor. The {% schema %} block at the end of the file declares what the editor shows: the section's name, its settings, the block types it accepts and the values it starts with.
Schema JSON must be valid. An invalid schema fails the whole theme upload, not just the section (Theme requirements).
Anatomy of a section
Markup first, schema last. The schema is removed before the file renders, so it never reaches the page.
Write the tag exactly as {% schema %} … {% endschema %}. The editor does not recognise the whitespace-control form {%- schema -%}, and a section written that way does not appear in the editor.
{% style %}
[data-section-id="{{ section.id }}"] .testimonial {
background: {{ section.settings.card_colour }};
}
{% endstyle %}
<div class="testimonials">
{% if section.settings.heading != blank %}
<h2>{{ section.settings.heading }}</h2>
{% endif %}
{% for block in section.blocks %}
<blockquote class="testimonial" data-block-id="{{ block.id }}">
<p>{{ block.settings.quote }}</p>
<cite>{{ block.settings.author }}</cite>
</blockquote>
{% endfor %}
</div>
{% schema %}
{
"name": "Testimonials",
"settings": [
{ "type": "text", "id": "heading", "label": "Heading", "default": "What customers say" },
{ "type": "color", "id": "card_colour", "label": "Card colour", "default": "#F5F5F4" }
],
"blocks": [
{
"type": "quote",
"name": "Quote",
"limit": 6,
"settings": [
{ "type": "textarea", "id": "quote", "label": "Quote" },
{ "type": "text", "id": "author", "label": "Author" }
]
}
],
"presets": [
{
"name": "Testimonials",
"blocks": [
{ "type": "quote", "settings": { "quote": "Arrived in two days.", "author": "Ayesha" } }
]
}
]
}
{% endschema %}Schema keys
| Key | Effect |
|---|---|
name | The section's name in the editor. Without it, the file name is used. |
settings | The section's settings. See Setting types. |
blocks | The block types the section accepts. Each has type, name and settings. limit caps how many of that type a merchant can add. |
presets | Only the first preset is read. Its settings and blocks become the section's starting values when a merchant adds it. |
class | Added to the wrapper <div> when the section is rendered with {% section %}. |
description, category, icon | Optional editor metadata. |
max_blocks | Parsed but not enforced. Use limit on each block type instead. |
Every section file with a parseable schema is offered under Add section, whether or not it has presets. To cap how many times a section appears on a page, use the sections map in theme.json (The manifest).
Setting types
Every setting needs type, id and label. default, info and placeholder are optional. select and radio need options. range needs min, max and step, and takes an optional unit. To show a setting only when another has a given value, add "condition": { "field": "layout", "value": "grid" }.
| Type | Editor control | Value in Liquid |
|---|---|---|
text | Single-line input | String |
textarea | Multi-line input | String |
richtext | Rich text editor | HTML string |
number | Number input | Number |
range | Slider | Number |
select | Dropdown | The chosen option's value |
radio | Dropdown (rendered the same as select) | The chosen option's value |
checkbox | Toggle. toggle and boolean are aliases | true or false |
color | Colour picker | Colour string, for example #F5F5F4 |
image | Media library. image_picker is an alias | Image URL |
video | Media library. video_picker is an alias | Video URL |
audio | Media library. audio_picker is an alias | Audio URL |
url | URL input | String |
link | Link picker | URL string |
link_list | Menu picker | Menu handle, for use with linklists |
product | Product picker. product_picker is an alias | Product ID |
products | Multi-product picker. product_list is an alias | Array of product IDs |
collection | Collection picker. collection_picker is an alias | Collection ID |
collections | Multi-collection picker. collection_list is an alias | Array of collection IDs |
datetime | Date and time picker | Date-time string |
Pickers store IDs, not objects. section.settings.product is a string, and the renderer does not look the product up for you. Use it to match against lists the page already has, such as products.
Any other type renders as a plain text input. That includes Shopify's header, paragraph, html, inline_richtext, font_picker, video_url and date, and a header or paragraph with no id is dropped altogether. color_scheme is accepted, but the section editor currently shows no control for it.
The global theme settings in config/settings_schema.json use a separate panel, which does support header, paragraph, checkbox and font_picker.
Reading settings
| Expression | Value |
|---|---|
section.id | The section's key in the JSON template, for example main |
section.type | The section file name, for example testimonials |
section.settings.<id> | A setting value |
section.<id> | The same value, copied onto the section. Settings named id, type, label, enabled, blocks, block_order or settings are not copied |
section.blocks | Blocks in the merchant's order |
block.id, block.type | The block's key and type |
block.settings.<id>, block.<id> | A block setting value |
block.blocks | Child blocks, for blocks of type group |
Whole-number values are converted to integers, so {% for i in (1..section.settings.columns) %} works.
Schema defaults are copied into a section when a merchant adds it. They are not applied at render time. A setting missing from a JSON template renders empty, so include every setting in your templates and use | default: for anything that may be absent.
Rendering sections
From a JSON template
Sections listed in a JSON template reach the Liquid template as section_list (How a request becomes a page). Render each one inside a wrapper that carries data-section-id. The theme editor needs that attribute to select the section.
{% for section in section_list %}
<div data-section-id="{{ section.id }}" data-section-type="{{ section.type }}">
{% render_section section %}
</div>
{% endfor %}Static sections with {% section %}
{% section 'announcement' %} renders sections/announcement.liquid in place. Merchants cannot edit a static section.
- Settings are the schema defaults only. They are available as
section.settings.<id>and as top-level variables. section.idis the file name andsection.blocksis empty.- The output is wrapped in
<div id="shopify-section-announcement" class="shopify-section">, plus the schema'sclass. There is nodata-section-id, so the editor cannot select it.
Section groups
{% sections 'header-group' %} reads sections/header-group.json, which uses the same shape as a JSON template, and renders each enabled section in order. This is how a layout gets a header and footer that merchants can edit. If the file does not exist it renders nothing.
No wrapper is added around grouped sections. Put data-section-id="{{ section.id }}" on each section's root element.
{
"sections": {
"announcement": { "type": "announcement-bar", "settings": { "text": "Free delivery over Rs. 5,000" } },
"header": { "type": "header", "settings": { "menu": "main-menu" } }
},
"order": ["announcement", "header"]
}Scoping CSS
section.id is unique within a page, so prefix selectors with [data-section-id="{{ section.id }}"] to keep styles from leaking between two copies of the same section. {% style %} renders the Liquid inside it and outputs a <style> element, which is how settings become CSS.
Two kinds of CSS are generated for you when a section renders through render_section or a section group:
- Custom CSS a merchant writes for a section in the editor is scoped to that section's
[data-section-id]and inserted before it. - Block appearance settings (spacing, colours, border, font size) become rules targeting
[data-block-id="…"]. Putdata-block-id="{{ block.id }}"on each block's root element, or those settings do nothing.
{% style %}
[data-section-id="{{ section.id }}"] {
padding-block: {{ section.settings.padding | default: 48 }}px;
}
[data-section-id="{{ section.id }}"] h2 {
text-align: {{ section.settings.alignment | default: 'left' }};
}
{% endstyle %}The theme editor
The editor previews your storefront in an iframe, with editor_mode set to true. Selecting and highlighting sections depends on three things in your theme:
data-section-idon every section wrapperdata-block-idon every block's root element- A small script that posts clicks to the editor and applies highlights
The platform does not inject that script. Loom's assets/theme.js includes one in its "Editor mode" block, so copy it. It posts section-clicked (sectionId) and block-clicked (blockId, sectionId) to the parent frame, and listens for highlight-section, clear-highlight, highlight-block and clear-block-highlight.
App blocks
A merchant can place an app's theme block inside a section. {% render_app_block block %} renders it at that block's position. Call it from your blocks loop for any block type you don't recognise. See Building an app for the app side.
Updated 15 September 2026