Talk Shop
Home
Learn More
About Us
Follow Us
Blog
Tools
Newsletter
Join Discord
Join

Community

  • Developers
  • Growth
  • Entrepreneurs
  • Support
  • Experts
  • Tools

Location

123 Mars, Crater City, Red Planet

(WiFi may be spotty)

Hours

Who has time for breaks? We're here 24/7!

Contact

hello@letstalkshop.com

Talk Shop
Talk Shop

Built for real builders. Not affiliated with Shopify Inc.

Home
Privacy
Terms
  1. Home
  2. >Blog
  3. >Theme Design
  4. >How to Build Custom Shopify Sections: A Practical Liquid Tutorial
Theme Design15 min read

How to Build Custom Shopify Sections: A Practical Liquid Tutorial

Learn how to build custom Shopify sections with Liquid and Online Store 2.0. Step-by-step code examples for hero banners, testimonials, FAQ accordions, and more.

Talk Shop

Talk Shop

Mar 16, 2026

How to Build Custom Shopify Sections: A Practical Liquid Tutorial

In this article

  • Custom Sections Are What Separate Template Stores From Professional Ones
  • How Sections Work in Online Store 2.0
  • Building a Hero Banner Section
  • Building a Testimonials Section With Blocks
  • Building an FAQ Accordion Section
  • Building a Product Feature Grid With Theme Blocks
  • Advanced Section Patterns
  • Performance Best Practices for Sections
  • Testing and Debugging Your Sections
  • Common Mistakes When Building Sections
  • Your Section Building Roadmap

Custom Sections Are What Separate Template Stores From Professional Ones

Every Shopify store starts with the same themes. The stores that stand out — the ones that feel like a brand instead of a template — are the ones with custom sections built specifically for their content and goals. Learning how to build custom Shopify sections is the single highest-leverage skill for any merchant or developer working with Shopify themes.

Sections are the modular building blocks of every page in Online Store 2.0. They're Liquid files with a visual editor interface, meaning you build them once with code and merchants customize them forever without touching a line of code. A well-built section library turns the Shopify theme editor from a limited tool into a full page builder.

This tutorial walks through building real sections from scratch — not theoretical concepts, but production-ready code you can paste into your theme today. If you're just getting started with Shopify development, sections are the best place to begin because results are immediate and visual.

How Sections Work in Online Store 2.0

Holographic comparison showing relationship between Shopify Sections, Blocks, and Snippets on a dark background.

Before writing code, understand how sections fit into the theme architecture.

The Section Lifecycle

  1. You create a .liquid file in the sections/ folder of your theme
  2. The file contains HTML/Liquid markup at the top and a {% schema %} block at the bottom
  3. The schema defines what settings and blocks appear in the theme editor
  4. Adding a presets array makes the section available in the "Add section" picker
  5. Merchants drag the section onto any page, configure settings, and publish

Section vs Block vs Snippet

These three concepts confuse beginners. Here's the distinction:

ConceptWhat It IsWhere It LivesMerchant-Editable?
SectionA self-contained page module with its own settings UIsections/ folderYes — full theme editor panel
BlockA repeatable content unit inside a sectionDefined in section schema or blocks/ folderYes — add, remove, reorder
SnippetA reusable code fragment included by sectionssnippets/ folderNo — code only, no editor UI

Sections are the building blocks merchants interact with. Blocks are the building blocks inside sections. Snippets are developer utilities that keep code DRY.

According to Shopify's official sections documentation, each JSON template can render up to 25 sections, and each section supports up to 50 blocks — more than enough for any page layout.

Building a Hero Banner Section

Let's start with the most common section: a hero banner with a background image, headline, subheadline, and CTA button. This pattern appears on virtually every Shopify store's homepage.

The Complete Code

Create sections/hero-banner.liquid:

liquidliquid
{{ 'section-hero-banner.css' | asset_url | stylesheet_tag }}

<section
  class="hero-banner"
  style="--hero-min-height: {{ section.settings.min_height }}px;"
>
  {%- if section.settings.image -%}
    <div class="hero-banner__media">
      <img
        src="{{ section.settings.image | image_url: width: 1920 }}"
        srcset="
          {{ section.settings.image | image_url: width: 768 }} 768w,
          {{ section.settings.image | image_url: width: 1200 }} 1200w,
          {{ section.settings.image | image_url: width: 1920 }} 1920w
        "
        sizes="100vw"
        alt="{{ section.settings.image.alt | escape }}"
        loading="eager"
        fetchpriority="high"
        width="1920"
        height="{{ 1920 | divided_by: section.settings.image.aspect_ratio | round }}"
      >
    </div>
  {%- endif -%}

  <div class="hero-banner__overlay" style="opacity: {{ section.settings.overlay_opacity | divided_by: 100.0 }};"></div>

  <div class="hero-banner__content hero-banner__content--{{ section.settings.text_alignment }}">
    {%- if section.settings.subheading != blank -%}
      <p class="hero-banner__subheading">{{ section.settings.subheading }}</p>
    {%- endif -%}

    {%- if section.settings.heading != blank -%}
      <h1 class="hero-banner__heading">{{ section.settings.heading }}</h1>
    {%- endif -%}

    {%- if section.settings.text != blank -%}
      <p class="hero-banner__text">{{ section.settings.text }}</p>
    {%- endif -%}

    {%- if section.settings.button_label != blank -%}
      <a href="{{ section.settings.button_link }}" class="hero-banner__button">
        {{ section.settings.button_label }}
      </a>
    {%- endif -%}
  </div>
</section>

{% schema %}
{
  "name": "Hero Banner",
  "tag": "section",
  "class": "hero-banner-section",
  "settings": [
    {
      "type": "image_picker",
      "id": "image",
      "label": "Background image"
    },
    {
      "type": "range",
      "id": "overlay_opacity",
      "min": 0,
      "max": 90,
      "step": 5,
      "default": 40,
      "unit": "%",
      "label": "Overlay opacity"
    },
    {
      "type": "range",
      "id": "min_height",
      "min": 300,
      "max": 800,
      "step": 50,
      "default": 500,
      "unit": "px",
      "label": "Minimum height"
    },
    {
      "type": "text",
      "id": "subheading",
      "label": "Subheading",
      "default": "New Collection"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Your headline here"
    },
    {
      "type": "textarea",
      "id": "text",
      "label": "Description"
    },
    {
      "type": "text",
      "id": "button_label",
      "label": "Button label",
      "default": "Shop Now"
    },
    {
      "type": "url",
      "id": "button_link",
      "label": "Button link"
    },
    {
      "type": "select",
      "id": "text_alignment",
      "label": "Text alignment",
      "options": [
        { "value": "left", "label": "Left" },
        { "value": "center", "label": "Center" },
        { "value": "right", "label": "Right" }
      ],
      "default": "center"
    }
  ],
  "presets": [
    {
      "name": "Hero Banner"
    }
  ]
}
{% endschema %}

The CSS

Create assets/section-hero-banner.css:

csscss
.hero-banner {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--hero-min-height, 500px);
  overflow: hidden;
}

.hero-banner__media {
  position: absolute;
  inset: 0;
}

.hero-banner__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.hero-banner__overlay {
  position: absolute;
  inset: 0;
  background: #000;
}

.hero-banner__content {
  position: relative;
  z-index: 2;
  max-width: 640px;
  padding: 2rem;
}

.hero-banner__content--center { text-align: center; }
.hero-banner__content--left { text-align: left; margin-right: auto; }
.hero-banner__content--right { text-align: right; margin-left: auto; }

.hero-banner__subheading {
  font-size: 0.75rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  margin-bottom: 1rem;
  opacity: 0.8;
}

.hero-banner__heading {
  font-size: clamp(2rem, 5vw, 3.5rem);
  line-height: 1.1;
  margin-bottom: 1rem;
}

.hero-banner__text {
  font-size: 1.1rem;
  margin-bottom: 2rem;
  opacity: 0.9;
}

.hero-banner__button {
  display: inline-block;
  padding: 0.875rem 2rem;
  background: var(--color-button, #fff);
  color: var(--color-button-text, #000);
  text-decoration: none;
  font-weight: 600;
  border-radius: 4px;
  transition: opacity 0.2s;
}

.hero-banner__button:hover { opacity: 0.85; }

What This Section Teaches

  • `image_picker` setting type for background images
  • `srcset` and `sizes` for responsive image loading
  • `range` settings for numeric sliders (height, opacity)
  • `select` setting for dropdown choices
  • CSS custom properties for passing Liquid values to CSS
  • `presets` to make the section available in the editor

Building a Testimonials Section With Blocks

Blocks are what make sections truly dynamic. Instead of hardcoding three testimonials, blocks let merchants add, remove, and reorder an unlimited number of testimonials through the editor.

The Complete Code

Create sections/testimonials.liquid:

liquidliquid
<section class="testimonials">
  {%- if section.settings.heading != blank -%}
    <h2 class="testimonials__heading">{{ section.settings.heading }}</h2>
  {%- endif -%}

  <div class="testimonials__grid">
    {%- for block in section.blocks -%}
      <div class="testimonials__card" {{ block.shopify_attributes }}>
        <div class="testimonials__stars">
          {%- for i in (1..block.settings.rating) -%}
            <span class="testimonials__star">★</span>
          {%- endfor -%}
        </div>
        {%- if block.settings.quote != blank -%}
          <blockquote class="testimonials__quote">
            {{ block.settings.quote }}
          </blockquote>
        {%- endif -%}
        <div class="testimonials__author">
          {%- if block.settings.avatar -%}
            <img
              src="{{ block.settings.avatar | image_url: width: 80 }}"
              alt="{{ block.settings.name }}"
              width="40"
              height="40"
              class="testimonials__avatar"
              loading="lazy"
            >
          {%- endif -%}
          <div>
            <strong>{{ block.settings.name }}</strong>
            {%- if block.settings.title != blank -%}
              <span class="testimonials__title">{{ block.settings.title }}</span>
            {%- endif -%}
          </div>
        </div>
      </div>
    {%- endfor -%}
  </div>
</section>

{% schema %}
{
  "name": "Testimonials",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "What Our Customers Say"
    }
  ],
  "blocks": [
    {
      "type": "testimonial",
      "name": "Testimonial",
      "settings": [
        {
          "type": "range",
          "id": "rating",
          "min": 1,
          "max": 5,
          "default": 5,
          "label": "Star rating"
        },
        {
          "type": "textarea",
          "id": "quote",
          "label": "Quote",
          "default": "This product changed how I run my business."
        },
        {
          "type": "text",
          "id": "name",
          "label": "Customer name",
          "default": "Jane D."
        },
        {
          "type": "text",
          "id": "title",
          "label": "Customer title",
          "default": "Store Owner"
        },
        {
          "type": "image_picker",
          "id": "avatar",
          "label": "Photo"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Testimonials",
      "blocks": [
        { "type": "testimonial" },
        { "type": "testimonial" },
        { "type": "testimonial" }
      ]
    }
  ]
}
{% endschema %}

Key Block Concepts

  • `block.shopify_attributes` — adds data attributes that connect the rendered block to the theme editor. Always include this on block wrapper elements.
  • Block presets — the blocks array in presets defines default blocks when the section is first added. Three testimonials by default gives merchants a starting point.
  • `for block in section.blocks` — iterates over all blocks the merchant has added. They control how many through the editor.

FirstPier's guide to mastering section schema covers advanced block patterns including nested blocks and conditional rendering.

Building an FAQ Accordion Section

Visualizing a dynamic FAQ accordion section with blue and cyan glows on a dark background.

FAQ sections are SEO gold — they answer questions Google shows in "People Also Ask" boxes and qualify for FAQ rich snippets with proper schema markup. For merchants focused on SEO performance, a well-built FAQ section does double duty.

The Complete Code

Create sections/faq-accordion.liquid:

liquidliquid
{{ 'section-faq-accordion.css' | asset_url | stylesheet_tag }}

<section class="faq">
  {%- if section.settings.heading != blank -%}
    <h2 class="faq__heading">{{ section.settings.heading }}</h2>
  {%- endif -%}

  <div class="faq__list">
    {%- for block in section.blocks -%}
      <details class="faq__item" {{ block.shopify_attributes }}>
        <summary class="faq__question">
          {{ block.settings.question }}
          <span class="faq__icon" aria-hidden="true">+</span>
        </summary>
        <div class="faq__answer">
          {{ block.settings.answer }}
        </div>
      </details>
    {%- endfor -%}
  </div>
</section>

{%- comment -%} FAQ Schema for Rich Snippets {%- endcomment -%}
{%- if section.blocks.size > 0 -%}
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {%- for block in section.blocks -%}
      {
        "@type": "Question",
        "name": {{ block.settings.question | json }},
        "acceptedAnswer": {
          "@type": "Answer",
          "text": {{ block.settings.answer | strip_html | json }}
        }
      }{% unless forloop.last %},{% endunless %}
    {%- endfor -%}
  ]
}
</script>
{%- endif -%}

{% schema %}
{
  "name": "FAQ Accordion",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Frequently Asked Questions"
    }
  ],
  "blocks": [
    {
      "type": "faq_item",
      "name": "Question",
      "settings": [
        {
          "type": "text",
          "id": "question",
          "label": "Question"
        },
        {
          "type": "richtext",
          "id": "answer",
          "label": "Answer"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "FAQ Accordion",
      "blocks": [
        { "type": "faq_item" },
        { "type": "faq_item" },
        { "type": "faq_item" }
      ]
    }
  ]
}
{% endschema %}

What Makes This Section Special

  • Native `<details>`/`<summary>` — accordion behavior without JavaScript. Works everywhere, accessible by default, keyboard navigable.
  • Built-in FAQ schema — the JSON-LD block at the bottom generates structured data for Google's FAQ rich snippets automatically. Every question the merchant adds becomes a potential rich result.
  • `richtext` setting type — allows formatted answers with bold, italic, links, and lists through the theme editor.

Building a Product Feature Grid With Theme Blocks

Shopify's blocks documentation introduces Theme Blocks — reusable block types that live in the /blocks folder and can be shared across multiple sections.

When to Use Theme Blocks vs Section Blocks

Section BlocksTheme Blocks
Defined inSection's {% schema %}Separate file in /blocks/
Reusable across sectionsNo — locked to one sectionYes — any section can use them
Best forSection-specific contentShared patterns (buttons, images, text)

For this feature grid, we'll use section blocks since the content is specific to this layout:

liquidliquid
<section class="feature-grid">
  {%- if section.settings.heading != blank -%}
    <h2 class="feature-grid__heading">{{ section.settings.heading }}</h2>
  {%- endif -%}

  <div class="feature-grid__items" style="--columns: {{ section.settings.columns }};">
    {%- for block in section.blocks -%}
      <div class="feature-grid__item" {{ block.shopify_attributes }}>
        {%- if block.settings.icon != blank -%}
          <div class="feature-grid__icon">
            <img
              src="{{ block.settings.icon | image_url: width: 96 }}"
              alt=""
              width="48"
              height="48"
              loading="lazy"
            >
          </div>
        {%- endif -%}
        <h3 class="feature-grid__title">{{ block.settings.title }}</h3>
        <p class="feature-grid__text">{{ block.settings.text }}</p>
      </div>
    {%- endfor -%}
  </div>
</section>

{% schema %}
{
  "name": "Feature Grid",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Why Choose Us"
    },
    {
      "type": "select",
      "id": "columns",
      "label": "Columns",
      "options": [
        { "value": "2", "label": "2" },
        { "value": "3", "label": "3" },
        { "value": "4", "label": "4" }
      ],
      "default": "3"
    }
  ],
  "blocks": [
    {
      "type": "feature",
      "name": "Feature",
      "settings": [
        {
          "type": "image_picker",
          "id": "icon",
          "label": "Icon"
        },
        {
          "type": "text",
          "id": "title",
          "label": "Title",
          "default": "Feature Name"
        },
        {
          "type": "textarea",
          "id": "text",
          "label": "Description",
          "default": "Describe this feature and its benefit."
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Feature Grid",
      "blocks": [
        { "type": "feature" },
        { "type": "feature" },
        { "type": "feature" }
      ]
    }
  ]
}
{% endschema %}

Advanced Section Patterns

A visual comparison showing a static and dynamic advanced Shopify section pattern.

Once you've built the basics, these patterns take your sections to the next level.

Dynamic Collection References

Let merchants select a collection and display its products:

liquidliquid
{%- assign collection = collections[section.settings.collection] -%}
{%- for product in collection.products limit: section.settings.limit -%}
  <div class="product-card">
    <a href="{{ product.url }}">
      <img src="{{ product.featured_image | image_url: width: 600 }}" alt="{{ product.title }}" loading="lazy">
      <h3>{{ product.title }}</h3>
      <p>{{ product.price | money }}</p>
    </a>
  </div>
{%- endfor -%}

Metafield-Powered Content

Pull dynamic content from metafields for truly custom data:

liquidliquid
{%- assign sizing_chart = product.metafields.custom.sizing_chart -%}
{%- if sizing_chart != blank -%}
  <div class="sizing-chart">
    {{ sizing_chart | metafield_tag }}
  </div>
{%- endif -%}

Responsive Background Video

liquidliquid
{%- if section.settings.video != blank -%}
  <video
    autoplay muted loop playsinline
    class="hero__video"
    poster="{{ section.settings.image | image_url: width: 1920 }}"
  >
    {%- for source in section.settings.video.sources -%}
      <source src="{{ source.url }}" type="{{ source.mime_type }}">
    {%- endfor -%}
  </video>
{%- endif -%}

Use the video setting type in your schema:

jsonjson
{
  "type": "video",
  "id": "video",
  "label": "Background video"
}

For deeper customization patterns, connect with the Shopify experts network — experienced developers can help with complex section architectures.

Performance Best Practices for Sections

Every section you build contributes to (or detracts from) page performance. These rules keep your sections fast:

Image Optimization

liquidliquid
{%- comment -%} Always use srcset for responsive images {%- endcomment -%}
<img
  src="{{ image | image_url: width: 800 }}"
  srcset="
    {{ image | image_url: width: 400 }} 400w,
    {{ image | image_url: width: 800 }} 800w,
    {{ image | image_url: width: 1200 }} 1200w
  "
  sizes="(max-width: 768px) 100vw, 50vw"
  alt="{{ image.alt | escape }}"
  loading="lazy"
  width="800"
  height="{{ 800 | divided_by: image.aspect_ratio | round }}"
>

CSS Loading Strategy

Only load a section's CSS when the section is on the page:

liquidliquid
{%- comment -%} At the top of your section {%- endcomment -%}
{{ 'section-my-section.css' | asset_url | stylesheet_tag }}

JavaScript: Progressive Enhancement

liquidliquid
{%- comment -%} Load JS only when needed, defer it {%- endcomment -%}
<script src="{{ 'section-carousel.js' | asset_url }}" defer></script>

Avoiding Common Performance Killers

Do ThisNot This
loading="lazy" on below-fold imagesLoad all images eagerly
Load section CSS via stylesheet_tagPut all CSS in theme.css
Use srcset with multiple sizesServe one huge image
Defer non-critical JavaScriptUse inline <script> tags
Set explicit width and height on imagesLet browsers guess dimensions (CLS)

Shogun's guide to custom Liquid sections covers additional performance patterns specific to high-traffic stores.

Testing and Debugging Your Sections

Visualizing optimized code performance and debug checks for Shopify sections.

The Theme Editor Test

After creating a section, verify:

  1. The section appears in the "Add section" picker
  2. All settings render correctly in the editor sidebar
  3. Changes save and preview in real-time
  4. Blocks can be added, removed, and reordered
  5. The section renders properly with empty/default values

Common Debugging Techniques

liquidliquid
{%- comment -%} Dump all section settings for debugging {%- endcomment -%}
<pre>{{ section.settings | json }}</pre>

{%- comment -%} Check block count {%- endcomment -%}
<p>Blocks: {{ section.blocks.size }}</p>

{%- comment -%} Verify a collection reference {%- endcomment -%}
{% assign col = collections[section.settings.collection] %}
{% if col %}
  Collection: {{ col.title }} ({{ col.products_count }} products)
{% else %}
  No collection selected
{% endif %}

Mobile Testing

Always test sections on real mobile devices. Common issues:

  • Text overflows on small screens
  • Touch targets too small (minimum 44x44px)
  • Images not responsive (missing srcset)
  • Horizontal scrolling from fixed widths

Instant.so's guide to custom Liquid covers mobile-first section patterns that prevent these issues.

Common Mistakes When Building Sections

Mistake #1: Missing Presets

Without a presets array in your schema, the section won't appear in the "Add section" picker. Merchants can't use a section they can't find.

Mistake #2: Hardcoding Content

Every piece of text, image, and link should come from settings or blocks. If a merchant can't change it through the editor, it shouldn't be in your section.

Mistake #3: Forgetting block.shopify_attributes

This attribute connects rendered blocks to the theme editor. Without it, clicking a block in the editor won't highlight it on the page — and merchants won't know what they're editing.

Mistake #4: Not Handling Empty States

Always check for blank values before rendering:

liquidliquid
{%- comment -%} Safe: handles missing content gracefully {%- endcomment -%}
{%- if section.settings.heading != blank -%}
  <h2>{{ section.settings.heading }}</h2>
{%- endif -%}

Mistake #5: Ignoring Accessibility

  • Add alt text to every image (use | escape filter)
  • Ensure color contrast meets WCAG standards
  • Make interactive elements keyboard accessible
  • Use semantic HTML (<nav>, <main>, <section>, <article>)

For help building accessible, performant sections, explore Talk Shop's theme design resources and our analytics guides for measuring section performance.

Your Section Building Roadmap

A visual journey roadmap showing progress through theme development milestones.

Learning how to build custom Shopify sections follows a natural progression. Here's what to build at each skill level:

Beginner (Week 1-2):

  • Hero banner with image, text, and CTA
  • Simple text + image section (side by side)
  • Announcement bar

Intermediate (Week 3-4):

  • Testimonials with blocks
  • FAQ accordion with schema markup
  • Featured collection grid with product cards

Advanced (Month 2+):

  • Custom product page sections with metafield integration
  • Multi-column sections with responsive breakpoints
  • Sections with JavaScript interactivity (carousels, tabs, modals)
  • Theme blocks shared across multiple sections

Each section you build adds to a reusable library that accelerates every future project. The best developers don't rebuild from scratch — they compose from proven sections. Share what you're building with the Talk Shop community — we love reviewing section code and sharing patterns.

Theme DesignShopify Development
Talk Shop

About Talk Shop

The Talk Shop team — insights from our community of Shopify developers, merchants, and experts.

Related Insights

Related

How to Create a Brand Identity for Your Shopify Store

Related

Shopify Schema Markup for SEO: The Complete Implementation Guide

The ecommerce newsletter that's actually useful.

Daily trends, teardowns, and tactics from the top 1% of ecommerce brands. Delivered every morning.

No spam. Unsubscribe anytime.

New

Business Name Generator

Generate unique, brandable business names with AI. Check domain availability instantly.

Generate Names

Talk Shop Daily

Daily ecommerce news, teardowns, and tactics.

No spam. Unsubscribe anytime.

Try our Business Name Generator