Sections, Blocks, and Schema Design Intermediate 12 min read

Blocks: Repeatable Content Patterns

Learn how to use blocks to create flexible, repeatable content areas that merchants can add, remove, and reorder in the theme editor.

Blocks are repeatable content units within sections. While section settings define global properties, blocks let merchants add multiple items of different types, reorder them, and customize each one individually. Think of blocks as the building blocks inside your building blocks.

🛠 Try the Schema Builder: Our Schema Builder makes it easy to define blocks with their settings, limits, and presets—all with visual editing and live JSON preview.

What Are Blocks?

Blocks are dynamic content units that merchants can:

  • Add new instances
  • Remove existing ones
  • Reorder by dragging
  • Customize each individually

Common use cases:

  • Slideshow slides
  • FAQ questions
  • Testimonials
  • Feature list items
  • Team members
  • Menu links

Defining Blocks in Schema

Blocks are defined in the blocks array of your schema:

{% schema %}
{
"name": "FAQ Section",
"blocks": [
{
"type": "question",
"name": "Question",
"settings": [
{
"type": "text",
"id": "question",
"label": "Question"
},
{
"type": "richtext",
"id": "answer",
"label": "Answer"
}
]
}
]
}
{% endschema %}

Each block definition needs:

  • type: A unique identifier (used in Liquid)
  • name: Display name in the editor
  • settings: Array of settings for this block type

Rendering Blocks

Loop through section.blocks to render each block:

<div class="faq-list">
{%- for block in section.blocks -%}
<div class="faq-item" {{ block.shopify_attributes }}>
<h3 class="faq-item__question">
{{ block.settings.question }}
</h3>
<div class="faq-item__answer">
{{ block.settings.answer }}
</div>
</div>
{%- endfor -%}
</div>

The block Object

Inside the loop, each block provides:

{{ block.type }} {# "question" #}
{{ block.id }} {# Unique block ID #}
{{ block.settings }} {# Block settings object #}
{{ block.settings.question }} {# Individual setting #}
{{ block.shopify_attributes }} {# Required for editor #}

Always Include shopify_attributes

The {{ block.shopify_attributes }} output is critical. It lets the theme editor:

  • Highlight the block when selected
  • Enable drag-and-drop reordering
  • Connect clicks to the settings panel
{# Good: Editor integration works #}
<div class="slide" {{ block.shopify_attributes }}>
{# Bad: Block won't be selectable in editor #}
<div class="slide">

Multiple Block Types

Sections can have different types of blocks. Use block.type to render each appropriately:

{%- for block in section.blocks -%}
{%- case block.type -%}
{%- when 'heading' -%}
<h2 {{ block.shopify_attributes }}>
{{ block.settings.text }}
</h2>
{%- when 'text' -%}
<div class="text-block" {{ block.shopify_attributes }}>
{{ block.settings.content }}
</div>
{%- when 'button' -%}
<a
href="{{ block.settings.link }}"
class="button"
{{ block.shopify_attributes }}
>
{{ block.settings.text }}
</a>
{%- when 'image' -%}
<div class="image-block" {{ block.shopify_attributes }}>
{{ block.settings.image | image_url: width: 800 | image_tag }}
</div>
{%- endcase -%}
{%- endfor -%}

With schema:

{% schema %}
{
"name": "Content Section",
"blocks": [
{
"type": "heading",
"name": "Heading",
"settings": [
{
"type": "text",
"id": "text",
"label": "Heading text"
}
]
},
{
"type": "text",
"name": "Text",
"settings": [
{
"type": "richtext",
"id": "content",
"label": "Content"
}
]
},
{
"type": "button",
"name": "Button",
"settings": [
{
"type": "text",
"id": "text",
"label": "Button text"
},
{
"type": "url",
"id": "link",
"label": "Button link"
}
]
},
{
"type": "image",
"name": "Image",
"settings": [
{
"type": "image_picker",
"id": "image",
"label": "Image"
}
]
}
]
}
{% endschema %}

Limiting Blocks

Maximum Blocks

Set max_blocks to limit how many blocks can be added:

{% schema %}
{
"name": "Slideshow",
"max_blocks": 5,
"blocks": [
{
"type": "slide",
"name": "Slide",
"limit": 5
}
]
}
{% endschema %}

Per-Type Limits

Limit specific block types:

{% schema %}
{
"name": "Hero Section",
"blocks": [
{
"type": "heading",
"name": "Heading",
"limit": 1
},
{
"type": "button",
"name": "Button",
"limit": 2
},
{
"type": "text",
"name": "Text"
}
]
}
{% endschema %}

The heading can only be added once, buttons up to twice, but text blocks have no limit.

Block Order in Presets

Pre-configure blocks in your presets:

{% schema %}
{
"name": "Slideshow",
"blocks": [
{
"type": "slide",
"name": "Slide",
"settings": [
{
"type": "image_picker",
"id": "image",
"label": "Image"
},
{
"type": "text",
"id": "heading",
"label": "Heading"
}
]
}
],
"presets": [
{
"name": "Slideshow",
"blocks": [
{
"type": "slide",
"settings": {
"heading": "Slide 1"
}
},
{
"type": "slide",
"settings": {
"heading": "Slide 2"
}
},
{
"type": "slide",
"settings": {
"heading": "Slide 3"
}
}
]
}
]
}
{% endschema %}

New installations start with three pre-configured slides.

Handling Empty Blocks

Always handle the case when no blocks exist:

{%- if section.blocks.size > 0 -%}
<div class="slideshow">
{%- for block in section.blocks -%}
<div class="slide" {{ block.shopify_attributes }}>
{{ block.settings.heading }}
</div>
{%- endfor -%}
</div>
{%- else -%}
{%- if request.design_mode -%}
<div class="placeholder">
<p>Add slides using the "Add block" button.</p>
</div>
{%- endif -%}
{%- endif -%}

Using forloop with Blocks

Access loop information for styling:

{%- for block in section.blocks -%}
<div
class="slide {% if forloop.first %}slide--active{% endif %}"
data-index="{{ forloop.index0 }}"
{{ block.shopify_attributes }}
>
<span class="slide-number">{{ forloop.index }} / {{ forloop.length }}</span>
{{ block.settings.content }}
</div>
{%- endfor -%}

Complete Example: Testimonials Section

{# sections/testimonials.liquid #}
<section class="testimonials">
<div class="container">
{%- if section.settings.heading != blank -%}
<h2 class="testimonials__heading">{{ section.settings.heading }}</h2>
{%- endif -%}
{%- if section.blocks.size > 0 -%}
<div class="testimonials__grid testimonials__grid--{{ section.settings.columns }}">
{%- for block in section.blocks -%}
<article class="testimonial-card" {{ block.shopify_attributes }}>
{%- if block.settings.rating > 0 -%}
<div class="testimonial-card__rating">
{%- for i in (1..5) -%}
<span class="star {% if i <= block.settings.rating %}star--filled{% endif %}">
</span>
{%- endfor -%}
</div>
{%- endif -%}
<blockquote class="testimonial-card__quote">
{{ block.settings.quote }}
</blockquote>
<footer class="testimonial-card__footer">
{%- if block.settings.image != blank -%}
<div class="testimonial-card__avatar">
{{ block.settings.image | image_url: width: 80 | image_tag }}
</div>
{%- endif -%}
<div class="testimonial-card__author">
<cite class="testimonial-card__name">
{{ block.settings.author }}
</cite>
{%- if block.settings.title != blank -%}
<span class="testimonial-card__title">
{{ block.settings.title }}
</span>
{%- endif -%}
</div>
</footer>
</article>
{%- endfor -%}
</div>
{%- else -%}
{%- if request.design_mode -%}
<div class="placeholder">
<p>Add testimonials using the block menu.</p>
</div>
{%- endif -%}
{%- endif -%}
</div>
</section>
{% schema %}
{
"name": "Testimonials",
"tag": "section",
"class": "section-testimonials",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "What our customers say"
},
{
"type": "select",
"id": "columns",
"label": "Columns",
"options": [
{ "value": "2", "label": "2" },
{ "value": "3", "label": "3" }
],
"default": "3"
}
],
"blocks": [
{
"type": "testimonial",
"name": "Testimonial",
"settings": [
{
"type": "range",
"id": "rating",
"label": "Star rating",
"min": 0,
"max": 5,
"step": 1,
"default": 5
},
{
"type": "textarea",
"id": "quote",
"label": "Quote",
"default": "This product changed my life!"
},
{
"type": "text",
"id": "author",
"label": "Author name",
"default": "Happy Customer"
},
{
"type": "text",
"id": "title",
"label": "Author title",
"default": "Verified Buyer"
},
{
"type": "image_picker",
"id": "image",
"label": "Author photo"
}
]
}
],
"presets": [
{
"name": "Testimonials",
"blocks": [
{
"type": "testimonial",
"settings": {
"quote": "Absolutely love this product!",
"author": "Sarah M."
}
},
{
"type": "testimonial",
"settings": {
"quote": "Best purchase I've ever made.",
"author": "John D."
}
},
{
"type": "testimonial",
"settings": {
"quote": "Exceeded all my expectations.",
"author": "Emily R."
}
}
]
}
]
}
{% endschema %}

Complete Example: Product Features

A section with multiple block types for product features:

{# sections/product-features.liquid #}
<section class="product-features">
{%- for block in section.blocks -%}
{%- case block.type -%}
{%- when 'feature' -%}
<div class="feature" {{ block.shopify_attributes }}>
{%- if block.settings.icon != blank -%}
<div class="feature__icon">
{% render 'icon', name: block.settings.icon %}
</div>
{%- endif -%}
<h3 class="feature__title">{{ block.settings.title }}</h3>
<p class="feature__text">{{ block.settings.text }}</p>
</div>
{%- when 'divider' -%}
<hr class="features-divider" {{ block.shopify_attributes }}>
{%- when 'heading' -%}
<h2 class="features-heading" {{ block.shopify_attributes }}>
{{ block.settings.text }}
</h2>
{%- endcase -%}
{%- endfor -%}
</section>
{% schema %}
{
"name": "Product Features",
"max_blocks": 12,
"blocks": [
{
"type": "feature",
"name": "Feature",
"settings": [
{
"type": "select",
"id": "icon",
"label": "Icon",
"options": [
{ "value": "", "label": "None" },
{ "value": "truck", "label": "Shipping" },
{ "value": "shield", "label": "Security" },
{ "value": "refresh", "label": "Returns" },
{ "value": "star", "label": "Quality" }
]
},
{
"type": "text",
"id": "title",
"label": "Title",
"default": "Feature title"
},
{
"type": "textarea",
"id": "text",
"label": "Description"
}
]
},
{
"type": "divider",
"name": "Divider",
"limit": 3
},
{
"type": "heading",
"name": "Heading",
"limit": 2,
"settings": [
{
"type": "text",
"id": "text",
"label": "Heading text"
}
]
}
],
"presets": [
{
"name": "Product Features",
"blocks": [
{
"type": "feature",
"settings": {
"icon": "truck",
"title": "Free Shipping",
"text": "On orders over $50"
}
},
{
"type": "feature",
"settings": {
"icon": "shield",
"title": "Secure Payment",
"text": "100% protected"
}
},
{
"type": "feature",
"settings": {
"icon": "refresh",
"title": "Easy Returns",
"text": "30-day guarantee"
}
}
]
}
]
}
{% endschema %}

Practice Exercise

Create a “Team Members” section with blocks that include:

  1. Photo (image picker)
  2. Name (text)
  3. Role (text)
  4. Bio (textarea)
  5. Social links (multiple URL fields)

Try implementing it yourself before checking the solution in the next lesson!

Key Takeaways

  1. Blocks are repeatable content units within sections
  2. section.blocks is an array you loop through
  3. block.type identifies which block type to render
  4. block.shopify_attributes is required for editor integration
  5. Multiple block types in one section enable flexible content
  6. max_blocks and limit** control how many blocks are allowed
  7. Presets can pre-configure blocks for new installations
  8. Handle empty blocks with placeholder content in design mode

What’s Next?

Now that you understand blocks, the next lesson is a Schema Types Deep Dive covering every available setting input type you can use in your schemas.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...