Introduction
Theme blocks are the foundation of Horizon’s architecture, enabling unprecedented flexibility and reusability. With 94 theme blocks in Horizon, this system represents the future of Shopify theme development.
Theme blocks are Liquid files in the blocks/ directory that can be dynamically added to any section that accepts { "type": "@theme" }. They’re prefixed with an underscore (_).
Core Concepts
What Makes a Theme Block?
A theme block is defined by three characteristics:
Location
File must be in the blocks/ directory
Naming
Filename must start with an underscore: _heading.liquid, _content.liquid
Schema
Must include a {% schema %} tag defining settings and configuration
Theme Block vs Regular Block
Theme Block
Section-Specific Block
{%- doc -%}
Renders a heading block.
{%- enddoc -%}
{% render 'text' , width: '100%' , block: block , fallback_text: text %}
{% schema %}
{
"name" : "t:names.heading" ,
"settings" : [
{
"type" : "richtext" ,
"id" : "text" ,
"label" : "t:settings.text"
}
]
}
{% endschema %}
Location : blocks/ directory
Usage : Can be added to ANY section with { "type": "@theme" }
Reusability : Maximum - used across multiple sections{%- comment -%} Defined inline in section schema {%- endcomment -%}
{% for block in section . blocks %}
{% if block . type == 'heading' %}
< h2 >{{ block . settings . text }}</ h2 >
{% endif %}
{% endfor %}
{% schema %}
{
"blocks" : [
{
"type" : "heading" ,
"name" : "Heading" ,
"settings" : [
{ "type" : "text" , "id" : "text" }
]
}
]
}
{% endschema %}
Location : Defined in section schema
Usage : Only available in that specific section
Reusability : Limited to one section
Horizon’s Theme Blocks Catalog
Horizon includes 94 theme blocks organized into categories:
Content Blocks
Text & Headings
_heading.liquid - Customizable headings (h1-h6)
_inline-text.liquid - Inline text elements
_content.liquid - Nestable content groups
_content-without-appearance.liquid - Content without styling
Dividers & Spacing
_divider.liquid - Visual dividers
Integrated spacing controls in all blocks
Media Blocks
Images
_image.liquid - Responsive images
_media.liquid - Image/video media
_media-without-appearance.liquid - Media without container
Advanced Media
_carousel-content.liquid - Carousel items
_layered-slide.liquid - Layered slideshow slides
_hotspot-product.liquid - Interactive hotspots
Product Blocks
_product-card.liquid - Complete product card
_product-card-gallery.liquid - Product image gallery
_product-card-group.liquid - Grouped product cards
_product-details.liquid - Product information
_featured-product.liquid - Featured product showcase
_featured-product-gallery.liquid - Featured product images
_featured-product-price.liquid - Product pricing
_featured-product-information-carousel.liquid - Product info carousel
Product List Blocks (3 blocks)
_product-list-content.liquid - Product list container
_product-list-button.liquid - Product list actions
Collection Blocks
Collection Components (5 blocks)
_collection-card.liquid - Collection card
_collection-card-image.liquid - Collection image
_collection-image.liquid - Collection banner image
_collection-info.liquid - Collection information
_collection-link.liquid - Collection navigation link
_inline-collection-title.liquid - Inline collection title
Blog Blocks
Blog Components (8 blocks)
_blog-post-card.liquid - Blog post card
_blog-post-content.liquid - Post content
_blog-post-description.liquid - Post excerpt
_blog-post-featured-image.liquid - Post featured image
_blog-post-image.liquid - Post inline image
_blog-post-info-text.liquid - Post metadata
_featured-blog-posts-card.liquid - Featured post card
_featured-blog-posts-image.liquid - Featured post image
_featured-blog-posts-title.liquid - Featured post title
Cart Blocks
_cart-products Cart line items display
_cart-summary Cart totals and checkout
_cart-title Cart page heading
Navigation Blocks
Accordion Blocks
_accordion-row Collapsible accordion row for FAQs and content organization
Layout Blocks
_card Generic card container
_marquee Scrolling marquee text
Block Architecture Patterns
1. Self-Contained Blocks
Simple blocks that render complete components:
< div class = "divider" style = "--divider-height: {{ block . settings . height }} px;" ></ div >
{% schema %}
{
"name" : "t:names.divider" ,
"settings" : [
{
"type" : "range" ,
"id" : "height" ,
"min" : 1 ,
"max" : 100 ,
"default" : 1
}
]
}
{% endschema %}
2. Wrapper Blocks with Delegation
Blocks that delegate rendering to snippets:
{%- doc -%}
Renders a heading block by delegating to the text snippet.
{%- enddoc -%}
{% render 'text' ,
width: '100%' ,
block: block ,
fallback_text: text
%}
{% schema %}
{
"name" : "t:names.heading" ,
"tag" : null ,
"settings" : [ ... ]
}
{% endschema %}
The "tag": null setting prevents Shopify from wrapping the block in a container, giving full control to the snippet.
3. Nestable Container Blocks
Blocks that accept other blocks as children:
{% capture children %}
{% content_for 'blocks' %}
{% endcapture %}
{% render 'group' ,
children: children ,
settings: block . settings ,
shopify_attributes: block . shopify_attributes
%}
{% schema %}
{
"name" : "t:names.content" ,
"tag" : null ,
"blocks" : [
{ "type" : "@theme" },
{ "type" : "@app" },
{ "type" : "_divider" }
],
"settings" : [
{
"type" : "select" ,
"id" : "horizontal_alignment_flex_direction_column" ,
"label" : "t:settings.alignment" ,
"options" : [
{ "value" : "flex-start" , "label" : "t:options.left" },
{ "value" : "center" , "label" : "t:options.center" },
{ "value" : "flex-end" , "label" : "t:options.right" }
]
},
{
"type" : "range" ,
"id" : "gap" ,
"label" : "t:settings.gap" ,
"min" : 0 ,
"max" : 100 ,
"unit" : "px" ,
"default" : 24
},
{
"type" : "checkbox" ,
"id" : "inherit_color_scheme" ,
"label" : "t:settings.inherit_color_scheme" ,
"default" : true
},
{
"type" : "color_scheme" ,
"id" : "color_scheme" ,
"label" : "t:settings.color_scheme" ,
"default" : "scheme-1" ,
"visible_if" : " {{ block . settings . inherit_color_scheme == false }} "
}
]
}
{% endschema %}
Nesting and Composition
How Nesting Works
Theme blocks can be nested multiple levels deep:
Section (_blocks.liquid)
└── Block: _content
├── Block: _heading
├── Block: _content (nested!)
│ ├── Block: _image
│ └── Block: button
└── Block: _divider
Rendering Flow
Section renders
_blocks.liquid section starts rendering
content_for captures blocks
{% content_for 'blocks' %} captures all child blocks
First-level blocks render
_content block starts rendering
Nested content_for
Nested _content block calls {% content_for 'blocks' %} again
Nested blocks render
_image and button blocks render inside nested content
Bubbles up
All rendered HTML bubbles up to the section
Practical Example
{%- comment -%} Section: _blocks.liquid {%- endcomment -%}
{% capture children %}
{% content_for 'blocks' %} {%- comment -%} Captures: _content block {%- endcomment -%}
{% endcapture %}
{%- comment -%} Block: _content.liquid {%- endcomment -%}
{% capture children %}
{% content_for 'blocks' %} {%- comment -%} Captures: _heading, _image {%- endcomment -%}
{% endcapture %}
Output:
< div class = "section" >
< div class = "group-block" > <!-- _content -->
< div class = "text-block" >
< h2 > Welcome </ h2 > <!-- _heading -->
</ div >
< div class = "image-block" >
< img src = "hero.jpg" > <!-- _image -->
</ div >
</ div >
</ div >
Static vs Dynamic Blocks
Static Blocks
Always present, cannot be removed:
sections/header.liquid (schema)
{
"blocks" : {
"header-logo" : {
"type" : "_header-logo" ,
"static" : true ,
"settings" : {
"hide_logo_on_home_page" : false
}
},
"header-menu" : {
"type" : "_header-menu" ,
"static" : true ,
"settings" : {
"menu" : "main-menu"
}
}
}
}
Rendered using content_for 'block':
{% content_for 'block' , type : '_header-logo' , id : 'header-logo' %}
{% content_for 'block' , type : '_header-menu' , id : 'header-menu' %}
Dynamic Blocks
Can be added, removed, reordered by merchants:
{
"sections" : {
"hero" : {
"type" : "hero" ,
"blocks" : {
"text_abc123" : {
"type" : "_heading" ,
"settings" : { "text" : "<h1>Welcome</h1>" }
},
"button_xyz789" : {
"type" : "button" ,
"settings" : { "label" : "Shop Now" }
}
},
"block_order" : [ "text_abc123" , "button_xyz789" ]
}
}
}
Rendered using content_for 'blocks':
{% capture children %}
{% content_for 'blocks' %} {%- comment -%} Renders all dynamic blocks {%- endcomment -%}
{% endcapture %}
Advanced Features
Context Passing
Pass data to static blocks:
sections/product-list.liquid
{% for product in collection . products %}
{% content_for 'block' ,
type : '_product-card' ,
id : 'static-product-card' ,
closest . product : product ,
closest . collection : collection
%}
{% endfor %}
Access in block:
blocks/_product-card.liquid
< div class = "product-card" >
< h3 >{{ closest . product . title }}</ h3 >
< p >{{ closest . product . price | money }}</ p >
< a href = " {{ closest . product . url }} " > View Product </ a >
</ div >
Shopify Attributes
Preserve theme editor functionality:
< div
class = "group-block"
{{ shopify_attributes }} {%- comment -%} Critical for theme editor! {%- endcomment -%}
>
{{ children }}
</ div >
Always include {{ shopify_attributes }} in the root element of a block, or the theme editor won’t be able to highlight and edit the block.
Conditional Settings with visible_if
blocks/_heading.liquid (schema)
{
"settings" : [
{
"type" : "checkbox" ,
"id" : "background" ,
"label" : "t:settings.background" ,
"default" : false
},
{
"type" : "color" ,
"id" : "background_color" ,
"label" : "t:settings.background_color" ,
"alpha" : true ,
"default" : "#00000026" ,
"visible_if" : "{{ block.settings.background }}"
},
{
"type" : "range" ,
"id" : "corner_radius" ,
"label" : "t:settings.corner_radius" ,
"min" : 0 ,
"max" : 50 ,
"default" : 0 ,
"visible_if" : "{{ block.settings.background }}"
}
]
}
Read-only Settings
Hide settings from merchants while preserving functionality:
{
"settings" : [
{
"type" : "checkbox" ,
"id" : "read_only" ,
"label" : "t:settings.read_only" ,
"visible_if" : "{{ false }}" ,
"default" : false
},
{
"type" : "richtext" ,
"id" : "text" ,
"label" : "t:settings.text" ,
"visible_if" : "{{ block.settings.read_only != true }}"
}
]
}
Creating Custom Theme Blocks
Basic Theme Block Template
blocks/_custom-block.liquid
{%- doc -%}
Description of what this block does.
@param {string} setting_name - Description of setting
{%- enddoc -%}
< div
class = "custom-block"
{{ shopify_attributes }}
style = "
--custom-property: {{ block . settings . custom_setting }} ;
"
>
{%- comment -%} Block content here {%- endcomment -%}
</ div >
{% schema %}
{
"name" : "t:names.custom_block" ,
"tag" : null ,
"settings" : [
{
"type" : "header" ,
"content" : "t:content.settings"
},
{
"type" : "text" ,
"id" : "custom_setting" ,
"label" : "t:settings.custom_setting" ,
"default" : "Default value"
}
]
}
{% endschema %}
Nestable Block Template
blocks/_custom-container.liquid
{%- doc -%}
A container block that accepts nested theme blocks.
{%- enddoc -%}
{% capture children %}
{% content_for 'blocks' %}
{% endcapture %}
< div
class = "custom-container"
{{ shopify_attributes }}
>
{{ children }}
</ div >
{% schema %}
{
"name" : "t:names.custom_container" ,
"tag" : null ,
"blocks" : [
{ "type" : "@theme" },
{ "type" : "@app" }
],
"settings" : [
{
"type" : "range" ,
"id" : "gap" ,
"label" : "t:settings.gap" ,
"min" : 0 ,
"max" : 100 ,
"unit" : "px" ,
"default" : 16
}
]
}
{% endschema %}
Best Practices
Keep blocks focused and single-purpose
Each block should do one thing well. Prefer composition over monolithic blocks. Good : _heading.liquid, _image.liquid, _button.liquid
Bad : _hero-with-everything.liquid
Always include {{ shopify_attributes }}
This is critical for theme editor functionality: < div {{ shopify_attributes }} >
<!-- Block content -->
</ div >
Document with {%- doc -%} tags
Use tag: null for custom rendering
Prevents Shopify’s default wrapper: {
"name" : "Custom Block" ,
"tag" : null ,
"settings" : [ ... ]
}
Leverage visible_if for better UX
Hide irrelevant settings: {
"id" : "advanced_setting" ,
"visible_if" : "{{ block.settings.enable_advanced }}"
}
Use inheritance for color schemes
Allow blocks to inherit parent colors: {
"type" : "checkbox" ,
"id" : "inherit_color_scheme" ,
"default" : true
}
Common Patterns
Pattern: Block with Optional Link
< div class = "card" {{ shopify_attributes }} >
{%- if block . settings . link != blank - %}
< a href = " {{ block . settings . link }} " class = "card__link" ></ a >
{%- endif -%}
{{ children }}
</ div >
{% schema %}
{
"settings" : [
{
"type" : "url" ,
"id" : "link" ,
"label" : "t:settings.link"
},
{
"type" : "checkbox" ,
"id" : "open_in_new_tab" ,
"label" : "t:settings.open_in_new_tab" ,
"visible_if" : " {{ block . settings . link != blank }} "
}
]
}
{% endschema %}
Pattern: Block with Media Background
blocks/_media-block.liquid
< div class = "media-block" {{ shopify_attributes }} >
< div class = "media-block__background" >
{% render 'background-media' ,
background_media: block . settings . background_media ,
background_video: block . settings . video ,
background_image: block . settings . background_image
%}
</ div >
{% capture children %}
{% content_for 'blocks' %}
{% endcapture %}
< div class = "media-block__content" >
{{ children }}
</ div >
</ div >
Pattern: Block with Responsive Settings
{
"settings" : [
{
"type" : "select" ,
"id" : "width" ,
"label" : "t:settings.width_desktop" ,
"options" : [
{ "value" : "25%" , "label" : "25%" },
{ "value" : "50%" , "label" : "50%" },
{ "value" : "100%" , "label" : "100%" }
],
"default" : "100%"
},
{
"type" : "select" ,
"id" : "width_mobile" ,
"label" : "t:settings.width_mobile" ,
"options" : [
{ "value" : "100%" , "label" : "100%" },
{ "value" : "50%" , "label" : "50%" }
],
"default" : "100%"
}
]
}
Debugging Theme Blocks
Inspecting Block Data
{%- comment -%} Temporary debugging code {%- endcomment -%}
< script >
console . log ({
blockType: ' {{ block . type }} ' ,
blockId: ' {{ block . id }} ' ,
settings: {{ block . settings | json }}
});
</ script >
Visual Preview Mode Detection
< div
class = "block"
{% if request.visual_preview_mode %}
data-shopify-visual-preview
{% endif %}
{{ shopify_attributes }}
>
Migration Guide
Converting Section Blocks to Theme Blocks
{% for block in section . blocks %}
{% case block . type %}
{% when 'heading' %}
< h2 >{{ block . settings . text }}</ h2 >
{% when 'button' %}
< a href = " {{ block . settings . link }} " >{{ block . settings . label }}</ a >
{% endcase %}
{% endfor %}
{% schema %}
{
"blocks" : [
{
"type" : "heading" ,
"name" : "Heading" ,
"settings" : [{ "type" : "text" , "id" : "text" }]
},
{
"type" : "button" ,
"name" : "Button" ,
"settings" : [
{ "type" : "text" , "id" : "label" },
{ "type" : "url" , "id" : "link" }
]
}
]
}
{% endschema %}
{% capture children %}
{% content_for 'blocks' %}
{% endcapture %}
{% render 'section' , section: section , children: children %}
{% schema %}
{
"blocks" : [
{ "type" : "@theme" },
{ "type" : "@app" }
]
}
{% endschema %}
{% render 'text' , block: block %}
{% schema %}
{
"name" : "Heading" ,
"settings" : [{ "type" : "richtext" , "id" : "text" }]
}
{% endschema %}
< a href = " {{ block . settings . link }} " class = "button" >
{{ block . settings . label }}
</ a >
{% schema %}
{
"name" : "Button" ,
"settings" : [
{ "type" : "text" , "id" : "label" },
{ "type" : "url" , "id" : "link" }
]
}
{% endschema %}
Performance Considerations
While nesting is powerful, excessive depth can impact render performance. Aim for 2-3 levels maximum.
Use static blocks for fixed structure
Static blocks render faster than dynamic blocks since their position is predetermined.
Lazy load images in blocks
{{ block . settings . image | image_url: width: 800 | image_tag: loading: 'lazy' }}
Scope CSS with {% stylesheet %}
Only loads when the block is used: {% stylesheet %}
.custom-block { /* styles */ }
{% endstylesheet %}
Next Steps
Theme Structure Explore sections, snippets, and templates
Development Guide Start building custom theme blocks
Block Reference Browse all 94 theme blocks
Liquid Storefronts Learn modern Liquid features