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 editorsettings: 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:
- Photo (image picker)
- Name (text)
- Role (text)
- Bio (textarea)
- Social links (multiple URL fields)
Try implementing it yourself before checking the solution in the next lesson!
Key Takeaways
- Blocks are repeatable content units within sections
section.blocksis an array you loop throughblock.typeidentifies which block type to renderblock.shopify_attributesis required for editor integration- Multiple block types in one section enable flexible content
max_blocksandlimit** control how many blocks are allowed- Presets can pre-configure blocks for new installations
- 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...