Introduction
Horizon showcases the latest Liquid Storefronts features, representing the future of Shopify theme development. These modern Liquid capabilities enable more flexible, maintainable, and powerful themes.
Liquid Storefronts is the evolution of Shopify’s templating system, introducing features like theme blocks, content_for, section groups, and more.
Theme Blocks
Theme blocks are the cornerstone of Liquid Storefronts, enabling reusable, composable components.
What are Theme Blocks?
Theme blocks are Liquid files in the blocks/ directory that:
Can be added to any section that accepts { "type": "@theme" }
Are prefixed with an underscore (_heading.liquid, _content.liquid)
Include their own schema and settings
Are fully reusable across sections
Basic Theme Block Structure
{%- doc -%}
Renders a heading block.
@param {string} text
{%- enddoc -%}
{% render 'text' , width: '100%' , block: block , fallback_text: text %}
{% schema %}
{
"name" : "t:names.heading" ,
"tag" : null ,
"settings" : [
{
"type" : "richtext" ,
"id" : "text" ,
"label" : "t:settings.text"
},
{
"type" : "select" ,
"id" : "type_preset" ,
"label" : "t:settings.preset" ,
"options" : [
{ "value" : "h1" , "label" : "t:options.h1" },
{ "value" : "h2" , "label" : "t:options.h2" },
{ "value" : "h3" , "label" : "t:options.h3" }
],
"default" : "h2"
},
{
"type" : "text_alignment" ,
"id" : "alignment" ,
"label" : "t:settings.alignment" ,
"default" : "left"
}
]
}
{% endschema %}
Accepting Theme Blocks in Sections
{% capture children %}
{% content_for 'blocks' %}
{% endcapture %}
{% render 'section' , section: section , children: children %}
{% schema %}
{
"name" : "t:names.section" ,
"blocks" : [
{ "type" : "@theme" }, // Accept ANY theme block
{ "type" : "@app" }, // Accept app blocks
{ "type" : "_divider" } // Specific theme block
]
}
{% endschema %}
The @theme wildcard allows merchants to add any theme block from your blocks/ directory to the section. This provides maximum flexibility without hardcoding which blocks are allowed. Benefits:
Merchants can compose custom layouts
No need to update section schema when adding new blocks
Enables true composability
Nestable Theme Blocks
Horizon’s _content block demonstrates nested blocks :
{% 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" ,
"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" ,
"min" : 0 ,
"max" : 100 ,
"default" : 24
}
]
}
{% endschema %}
Content block added to section
Merchant adds _content block to a section
Nested blocks added
Merchant adds _heading, _image, and button blocks inside the _content block
Rendered with proper nesting
The content_for 'blocks' captures nested children, and group snippet renders them with proper layout
content_for Tag
The {% content_for %} tag is a powerful Liquid Storefronts feature for rendering dynamic content.
Basic Usage
{% capture children %}
{% content_for 'blocks' %}
{% endcapture %}
{{ children }}
Advanced: content_for with Static Blocks
< div class = "section-carousel" >
{%- comment -%} Static header block {%- endcomment -%}
{% content_for 'block' , type : 'group' , id : 'static-header' %}
{%- comment -%} Static carousel content {%- endcomment -%}
{% content_for 'block' , type : '_carousel-content' , id : 'static-carousel-content' %}
</ div >
Static blocks are defined in the section schema with "static": true and always render in the same position.
content_for with Context
Pass additional context to blocks:
{% content_for 'block' ,
type : '_product-card' ,
id : 'static-product-card' ,
closest . product : product ,
closest . collection : collection
%}
Blocks can access context via closest object:
blocks/_product-card.liquid
< div class = "product-card" >
< h3 >{{ closest . product . title }}</ h3 >
< p >{{ closest . product . price | money }}</ p >
</ div >
Section Groups
Section groups allow multiple sections to be rendered as a cohesive unit.
< div id = "header-group" >
{% sections 'header-group' %}
</ div >
sections/header-group.json
{
"type" : "header" ,
"name" : "t:names.header" ,
"sections" : {
"header_announcements" : {
"type" : "header-announcements" ,
"blocks" : {
"announcement" : {
"type" : "_announcement" ,
"settings" : { "text" : "Welcome to our store" }
}
}
},
"header_section" : {
"type" : "header" ,
"blocks" : {
"header-logo" : { "type" : "_header-logo" , "static" : true },
"header-menu" : { "type" : "_header-menu" , "static" : true }
},
"settings" : {
"logo_position" : "left" ,
"enable_sticky_header" : "always"
}
}
},
"order" : [ "header_announcements" , "header_section" ]
}
Benefits
Logical grouping of related sections
Independent customization
Better performance (grouped rendering)
Cleaner template files
Use Cases
Header (announcements + navigation)
Footer (links + newsletter + social)
Product page (details + recommendations)
Static vs Dynamic Blocks
Static Blocks
Static blocks always appear and cannot be removed by merchants:
{
"blocks" : {
"header-logo" : {
"type" : "_header-logo" ,
"static" : true ,
"settings" : { "hide_logo_on_home_page" : false }
}
}
}
Dynamic Blocks
Dynamic blocks can be added, removed, and reordered:
{
"blocks" : {
"text_123" : {
"type" : "_heading" ,
"settings" : { "text" : "<h2>Welcome</h2>" }
},
"image_456" : {
"type" : "_image" ,
"settings" : { "image" : "shopify://shop_images/hero.jpg" }
}
},
"block_order" : [ "text_123" , "image_456" ]
}
Documentation Tags
Horizon uses {%- doc -%} tags for inline documentation:
{%- doc -%}
Renders a wrapper section
@param {section} section - The section object
@param {string} children - The children of the section
@param {boolean} [apply_overlay] - Optional overlay flag
@example
{% render 'section' , section: section , children: children %}
{%- enddoc -%}
Self-documenting code
Better developer experience
Clear parameter expectations
Usage examples embedded in code
Document all snippets and blocks
Include parameter types
Mark optional parameters with []
Provide usage examples
Visible_if Conditions
Conditionally show/hide settings in the theme editor:
{
"settings" : [
{
"type" : "checkbox" ,
"id" : "toggle_overlay" ,
"label" : "t:settings.background_overlay"
},
{
"type" : "color" ,
"id" : "overlay_color" ,
"label" : "t:settings.overlay_color" ,
"visible_if" : "{{ section.settings.toggle_overlay }}"
},
{
"type" : "select" ,
"id" : "overlay_style" ,
"options" : [
{ "value" : "solid" , "label" : "t:options.solid" },
{ "value" : "gradient" , "label" : "t:options.gradient" }
],
"visible_if" : "{{ section.settings.toggle_overlay }}"
},
{
"type" : "select" ,
"id" : "gradient_direction" ,
"options" : [
{ "value" : "to top" , "label" : "t:options.up" },
{ "value" : "to bottom" , "label" : "t:options.down" }
],
"visible_if" : "{{ section.settings.toggle_overlay and section.settings.overlay_style == 'gradient' }}"
}
]
}
visible_if creates dynamic, contextual settings that only appear when relevant, improving the merchant experience.
Advanced Liquid Patterns
Liquid Tag
The {% liquid %} tag allows multi-line Liquid without repetitive delimiters:
{% liquid
assign media_count = 0
assign media_1 = 'none'
assign media_2 = 'none'
if section . settings . image_1 != blank and section . settings . media_type_1 == 'image'
assign media_1 = 'image'
assign media_count = media_count | plus: 1
endif
if section . settings . video_1 != blank and section . settings . media_type_1 == 'video'
assign media_1 = 'video'
assign media_count = media_count | plus: 1
endif
%}
Vs. traditional syntax:
{% assign media_count = 0 %}
{% assign media_1 = 'none' %}
{% if section . settings . image_1 != blank %}
{% assign media_1 = 'image' %}
{% endif %}
Inline Stylesheets
Scope CSS to specific components:
snippets/bento-grid.liquid
{% for item in items %}
< div class = "bento-box__item" >
{{ item }}
</ div >
{% endfor %}
{% stylesheet %}
.bento-box {
display : grid ;
grid-template-columns : repeat ( 12 , 1 fr );
column-gap : var ( --bento-gap );
}
.bento-box__item :nth-child ( 1 ) {
grid-area : A;
}
{% endstylesheet %}
Benefits of Inline Stylesheets
Scoped styles : CSS only loads when the snippet is used
Co-location : Styles live with markup
Performance : Automatic critical CSS extraction
Maintainability : Easier to update components
Translation Integration
Horizon uses t: prefixes for all translatable strings:
{
"name" : "t:names.heading" ,
"settings" : [
{
"type" : "text" ,
"label" : "t:settings.text" ,
"info" : "t:info.heading_guidelines"
}
]
}
Translation files in locales/:
{
"names" : {
"heading" : "Heading" ,
"section" : "Custom Section"
},
"settings" : {
"text" : "Text content" ,
"alignment" : "Alignment"
},
"options" : {
"left" : "Left" ,
"center" : "Center" ,
"right" : "Right"
}
}
Color Schemes
Horizon uses the color scheme system:
{
"type" : "color_scheme" ,
"id" : "color_scheme" ,
"label" : "t:settings.color_scheme" ,
"default" : "scheme-1"
}
Applied via CSS classes:
< div class = "section color- {{ section . settings . color_scheme }} " >
<!-- Content -->
</ div >
Color scheme CSS variables:
.color-scheme-1 {
--color-background : #ffffff ;
--color-foreground : #121212 ;
--color-primary : #007ace ;
}
Request Object Enhancements
Visual Preview Mode
< div
class = "section"
{% if request.visual_preview_mode %}
data-shopify-visual-preview
{% endif %}
>
Design Mode Detection
< html
{% if request.design_mode %}
class = "shopify-design-mode"
{% endif %}
>
{% if request . design_mode %}
{%- render 'theme-editor' - %}
{% endif %}
Performance Features
SVH Units
Horizon uses svh (small viewport height) for better mobile support:
style="
{% if section . settings . section_height == 'custom' %}
--section-min-height: {{ section . settings . section_height_custom }} svh;
{% elsif section . settings . section_height == 'full-screen' %}
--section-min-height: 100svh;
{% endif %}
"
Lazy Loading
{{ image | image_url: width: 800 | image_tag: loading: 'lazy' }}
Preloading Critical Assets
< link rel = "preload" as = "image" href = " {{ section . settings . image | image_url: width: 1200 }} " >
Best Practices
Use theme blocks for reusability
Create small, focused theme blocks that do one thing well. Compose complex layouts by combining multiple blocks.
Leverage content_for for flexibility
Use content_for 'blocks' to capture dynamic content and content_for 'block' for static blocks.
Document with {%- doc -%} tags
Always document snippets and complex blocks with parameter descriptions and examples.
Use visible_if for conditional settings
Hide irrelevant settings to create a better merchant experience.
Embrace the {% liquid %} tag
Use {% liquid %} for complex logic to improve readability.
Scope CSS with {% stylesheet %}
Keep CSS co-located with components using inline stylesheet tags.
Examples from Horizon
Complete Section Example
{% capture children %}
{% content_for 'blocks' %}
{% endcapture %}
{% render 'section' , section: section , children: children %}
{% schema %}
{
"name" : "t:names.section" ,
"class" : "section-wrapper" ,
"blocks" : [
{ "type" : "@theme" },
{ "type" : "@app" },
{ "type" : "_divider" }
],
"settings" : [
{
"type" : "select" ,
"id" : "content_direction" ,
"label" : "t:settings.direction" ,
"options" : [
{ "value" : "column" , "label" : "t:options.vertical" },
{ "value" : "row" , "label" : "t:options.horizontal" }
],
"default" : "column"
},
{
"type" : "range" ,
"id" : "gap" ,
"label" : "t:settings.gap" ,
"min" : 0 ,
"max" : 100 ,
"default" : 12
},
{
"type" : "color_scheme" ,
"id" : "color_scheme" ,
"label" : "t:settings.color_scheme" ,
"default" : "scheme-1"
}
],
"presets" : [
{
"name" : "t:names.custom_section" ,
"category" : "t:categories.layout"
}
]
}
{% endschema %}
Migration from Legacy Patterns
{%- section 'header' - %}
{% for block in section . blocks %}
{% case block . type %}
{% when 'heading' %}
< h2 >{{ block . settings . text }}</ h2 >
{% when 'image' %}
{{ block . settings . image | image_url: width: 800 | image_tag }}
{% endcase %}
{% endfor %}
{% sections 'header-group' %}
{% capture children %}
{% content_for 'blocks' %}
{% endcapture %}
{{ children }}
With theme blocks:
blocks/_heading.liquid
blocks/_image.liquid
Next Steps
Theme Blocks Deep Dive Learn advanced theme blocks patterns
Theme Structure Understand sections, blocks, and snippets
Development Guide Start building with Liquid Storefronts
API Reference Official Shopify documentation