How to Build a Custom Shopify Section with Liquid: A Step-by-Step Guide
A practical, step-by-step guide to building a merchant-editable custom Shopify section with Liquid: schema settings, repeatable blocks, presets, scoped CSS, and the mistakes that break sections.

Yash Nandvana
Full Stack Developer

1. Introduction
Every Shopify merchant eventually asks for the same thing: a section on the homepage that does exactly what their store needs, editable from the theme editor, without installing another app. That is precisely what custom Shopify sections are for.
A section is a self-contained block of Liquid, HTML, CSS, and JavaScript that a merchant can add, reorder, and configure from the theme editor. Unlike editing theme code directly for every change, a well-built section exposes its content as settings: headings, images, colours, and repeatable blocks that non-developers can edit safely.
This guide walks through building a complete custom section from scratch: the file structure, the schema that powers the editor, repeatable blocks, and the mistakes that most commonly break a section. You need a Shopify theme you can edit (a development store or a duplicate of a live theme) and basic familiarity with HTML and Liquid.
2. How a Section Works
A section lives in the sections folder of a theme as a single .liquid file. That one file contains three parts:
{% schema %} tags that tells the theme editor what settings and blocks the section offers.{% stylesheet %} tags, and JavaScript loaded from the theme's assets.The key idea is the separation between code and content. The developer writes the markup once; the merchant changes the content through settings. The section reads those settings at render time through the section object: section.settings.heading for a section-level setting, and block.settings.quote for a setting belonging to one block.
Sections also support presets. A preset defines the default configuration a section gets when a merchant first adds it, including which blocks it starts with. Without a preset, a section still works, but the merchant starts from an empty state — so always include one.
3. The Section We Will Build
We will build a testimonials section: a heading, an optional subheading, and a row of repeatable testimonial cards, each with a quote, an author name, and an optional photo. The merchant can add up to six testimonials, change the background colour, and choose how many cards appear per row.
Create the file sections/testimonials.liquid in your theme and add the markup first:
<div class="testimonials-section" style="background-color: {{ section.settings.background_color }};">
<div class="page-width">
{% if section.settings.heading != blank %}
<h2>{{ section.settings.heading | escape }}</h2>
{% endif %}
{% if section.settings.subheading != blank %}
<p class="testimonials-subheading">{{ section.settings.subheading }}</p>
{% endif %}
<div class="testimonials-grid testimonials-grid--{{ section.settings.cards_per_row }}">
{% for block in section.blocks %}
<figure class="testimonial-card" {{ block.shopify_attributes }}>
{% if block.settings.photo != blank %}
<img
src="{{ block.settings.photo | image_url: width: 200 }}"
alt="{{ block.settings.author | escape }}"
width="100"
height="100"
loading="lazy"
>
{% endif %}
<blockquote>{{ block.settings.quote }}</blockquote>
{% if block.settings.author != blank %}
<figcaption>{{ block.settings.author | escape }}</figcaption>
{% endif %}
</figure>
{% endfor %}
</div>
</div>
</div>A few details worth noting:
block.shopify_attributes is required on the block wrapper. It lets the theme editor identify the block so the merchant can click a card in the preview and jump straight to its settings.| escape on plain-text settings prevents merchant-entered text from breaking the markup. The quote uses a richtext setting type, which already outputs safe HTML, so it is rendered without escaping.| image_url: width: 200 asks Shopify's CDN for a sensibly sized image instead of the full original upload.loading="lazy" keeps off-screen testimonial photos out of the initial page load.4. The Schema: Settings, Blocks, and Presets
Now add the schema at the bottom of the same file. This is the part the theme editor reads:
{% schema %}
{
"name": "Testimonials",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "What our customers say"
},
{
"type": "richtext",
"id": "subheading",
"label": "Subheading"
},
{
"type": "color",
"id": "background_color",
"label": "Background colour",
"default": "#f7f5f2"
},
{
"type": "select",
"id": "cards_per_row",
"label": "Cards per row",
"options": [
{ "value": "2", "label": "2" },
{ "value": "3", "label": "3" }
],
"default": "3"
}
],
"blocks": [
{
"type": "testimonial",
"name": "Testimonial",
"settings": [
{
"type": "richtext",
"id": "quote",
"label": "Quote",
"default": "<p>This store made ordering effortless.</p>"
},
{
"type": "text",
"id": "author",
"label": "Author",
"default": "Happy customer"
},
{
"type": "image_picker",
"id": "photo",
"label": "Photo"
}
]
}
],
"max_blocks": 6,
"presets": [
{
"name": "Testimonials",
"blocks": [
{ "type": "testimonial" },
{ "type": "testimonial" },
{ "type": "testimonial" }
]
}
]
}
{% endschema %}How the pieces fit together:
type, an id, and a label. The id is what you reference in Liquid (section.settings.heading); the label is what the merchant sees.color renders a colour picker, image_picker opens the media library, select renders a dropdown, and richtext gives a small formatted-text editor. Use the most specific type available instead of plain text wherever you can.max_blocks caps the number of testimonials at six, which protects the layout from a merchant accidentally adding twenty cards.One rule that saves hours of debugging: the schema must be valid JSON. A trailing comma or a mismatched bracket will not throw a friendly error — the section simply fails to appear in the editor. Paste the schema into a JSON validator before saving if anything looks wrong.
5. Styling the Section
Add scoped CSS with the {% stylesheet %} tag. Styles declared here are scoped to the section, so class names like .testimonial-card will not leak into the rest of the theme:
{% stylesheet %}
.testimonials-section {
padding: 48px 0;
}
.testimonials-grid {
display: grid;
gap: 24px;
margin-top: 32px;
}
.testimonials-grid--2 {
grid-template-columns: repeat(2, 1fr);
}
.testimonials-grid--3 {
grid-template-columns: repeat(3, 1fr);
}
@media screen and (max-width: 749px) {
.testimonials-grid--2,
.testimonials-grid--3 {
grid-template-columns: 1fr;
}
}
.testimonial-card {
background: #ffffff;
border-radius: 12px;
padding: 24px;
}
.testimonial-card img {
border-radius: 50%;
object-fit: cover;
}
{% endstylesheet %}Note how the grid class in the markup (testimonials-grid--{{ section.settings.cards_per_row }}) matches the modifier classes in the CSS. The merchant's "Cards per row" dropdown therefore switches the layout with no extra code. The mobile media query collapses everything to one column, which is the behaviour shoppers on phones expect.
For JavaScript, prefer a theme asset over inline scripts: add assets/testimonials.js to the theme and load it from the section with {{ 'testimonials.js' | asset_url | script_tag }}. Inline scripts inside sections run once per section instance and are harder to cache; an asset file is cached by the browser and shared across pages.
6. Testing in the Theme Editor
Save the file, then open the theme editor (Online Store, then Themes, then Customize) and add the section to a page template. Check four things:
block.shopify_attributes is missing from the block wrapper.Also test with empty content: delete the heading and all blocks and confirm the section degrades gracefully instead of rendering empty boxes. The != blank checks in the markup handle this, but only if you actually test the empty state.
7. Common Mistakes
background_color but the markup reads background_colour. Liquid silently outputs nothing for an unknown setting, so this fails without an error message. Copy ids, do not retype them.< or & can break the layout. Escape every plain-text output; only richtext settings are safe to output raw.image_url filter serves multi-megabyte originals. Always request the width you actually display.8. When to Use a Section, an App Block, or an App
A custom section is the right choice when the content belongs to the theme and the merchant edits it in the theme editor. If the feature needs data from an external service, server-side logic, or a merchant dashboard, a Shopify app is the better fit — and an app can still expose its UI as a theme app extension block so merchants can place it from the same editor. The two approaches compose: sections for theme-owned presentation, app blocks for app-powered features.
9. Checklist Before Shipping
block.shopify_attributes.image_url with an explicit width.{% stylesheet %} and JavaScript lives in an asset file.Build one section this way and the pattern repeats for almost everything else a store needs: announcement bars, feature grids, image-with-text blocks, FAQs, and collection showcases. The schema is the contract between your code and the merchant — get it right, and the section practically maintains itself.
Conclusion
A good custom section is not impressive because of clever Liquid. It is impressive because a merchant can use it without a developer in the room: add it, edit plain-language settings, reorder blocks, and see the storefront update as expected.
That comes from the basics covered here — a valid schema, sensible defaults, a preset that shows the intended design, block.shopify_attributes on every block, scoped styles, and testing with real merchant actions instead of only the happy path. Get those right, and one section becomes a pattern you can reuse across stores. Get them wrong, and every section becomes its own small maintenance problem.
Next Recommended Reads

Yash Nandvana• Full Stack Developer
Full Stack & Shopify Developer building scalable web apps, developer tools, and AI solutions.
Learn more about YashRelated Articles
Shopify Webhooks: A Complete Guide for Developers
A comprehensive, production-tested guide to Shopify webhooks: HMAC signature verification, raw body handling, idempotency with Redis, queue architecture, and local testing.
Shopify GraphQL Admin API: A Practical Guide for Developers
A comprehensive, production-oriented guide to Shopify's GraphQL Admin API: queries, mutations, pagination, rate limit cost calculation, userErrors checking, and bulk operations.
Shopify Store Speed Optimization: A Practical Guide for Developers
A practical guide to making Shopify stores faster: measuring real pages, fixing oversized images, auditing apps, scoping theme code, and handling fonts, videos, and third-party scripts.
