Skip to main content

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

blocks/_heading.liquid
{%- 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

sections/_blocks.liquid
{% 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:
blocks/_content.liquid
{% 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 %}
1

Content block added to section

Merchant adds _content block to a section
2

Nested blocks added

Merchant adds _heading, _image, and button blocks inside the _content block
3

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

sections/carousel.liquid
<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.

Header Group

layout/theme.liquid
<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

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, 1fr);
    column-gap: var(--bento-gap);
  }

  .bento-box__item:nth-child(1) {
    grid-area: A;
  }
{% endstylesheet %}
  • 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/:
locales/en.default.json
{
  "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

Create small, focused theme blocks that do one thing well. Compose complex layouts by combining multiple blocks.
Use content_for 'blocks' to capture dynamic content and content_for 'block' for static blocks.
Always document snippets and complex blocks with parameter descriptions and examples.
Hide irrelevant settings to create a better merchant experience.
Use {% liquid %} for complex logic to improve readability.
Keep CSS co-located with components using inline stylesheet tags.

Examples from Horizon

Complete Section Example

sections/_blocks.liquid
{% 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 %}

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

Build docs developers (and LLMs) love