Skip to main content

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:
1

Location

File must be in the blocks/ directory
2

Naming

Filename must start with an underscore: _heading.liquid, _content.liquid
3

Schema

Must include a {% schema %} tag defining settings and configuration

Theme Block vs Regular Block

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

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-content.liquid - Product list container
  • _product-list-button.liquid - Product list actions

Collection 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-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
  • _header-logo.liquid - Site logo with responsive sizing
  • _header-menu.liquid - Main navigation menu
  • _announcement.liquid - Announcement bar item

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:
blocks/_divider.liquid
<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:
blocks/_heading.liquid
{%- 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:
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",
      "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

1

Section renders

_blocks.liquid section starts rendering
2

content_for captures blocks

{% content_for 'blocks' %} captures all child blocks
3

First-level blocks render

_content block starts rendering
4

Nested content_for

Nested _content block calls {% content_for 'blocks' %} again
5

Nested blocks render

_image and button blocks render inside nested content
6

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:
templates/index.json
{
  "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:
blocks/_content.liquid
<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

Each block should do one thing well. Prefer composition over monolithic blocks.Good: _heading.liquid, _image.liquid, _button.liquid
Bad: _hero-with-everything.liquid
This is critical for theme editor functionality:
<div {{ shopify_attributes }}>
  <!-- Block content -->
</div>
{%- doc -%}
  Clear description of the block's purpose.

  @param {type} name - Parameter description
{%- enddoc -%}
Prevents Shopify’s default wrapper:
{
  "name": "Custom Block",
  "tag": null,
  "settings": [...]
}
Hide irrelevant settings:
{
  "id": "advanced_setting",
  "visible_if": "{{ block.settings.enable_advanced }}"
}
Allow blocks to inherit parent colors:
{
  "type": "checkbox",
  "id": "inherit_color_scheme",
  "default": true
}

Common Patterns

blocks/_card.liquid
<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

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

Performance Considerations

While nesting is powerful, excessive depth can impact render performance. Aim for 2-3 levels maximum.
Static blocks render faster than dynamic blocks since their position is predetermined.
{{ block.settings.image | image_url: width: 800 | image_tag: loading: 'lazy' }}
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

Build docs developers (and LLMs) love