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. >Shopify Development
  4. >Shopify Custom Sections Liquid Tutorial: Build 5 Sections From Scratch
Shopify Development12 min read

Shopify Custom Sections Liquid Tutorial: Build 5 Sections From Scratch

A hands-on Liquid tutorial that walks you through building 5 production-ready custom sections — announcement bar, image with text, product showcase, newsletter signup, and Instagram feed.

Talk Shop

Talk Shop

Mar 18, 2026

Shopify Custom Sections Liquid Tutorial: Build 5 Sections From Scratch

In this article

  • Why Custom Sections Are the Foundation of Every Great Shopify Store
  • How Shopify Sections Work (Quick Refresher)
  • Section 1: Dismissable Announcement Bar
  • Section 2: Image With Text (Reversible Split Layout)
  • Section 3: Product Showcase With Variant Selector
  • Section 4: Newsletter Signup With Shopify Customer API
  • Section 5: Social Media / Instagram-Style Grid
  • Comparing All Five Sections at a Glance
  • Testing and Debugging Your Custom Sections
  • Performance Tips for Production Sections
  • Extending These Sections With Blocks and App Blocks
  • Putting It All Together: Your Section Development Workflow

Why Custom Sections Are the Foundation of Every Great Shopify Store

Every Shopify theme is built from sections. They are the building blocks that merchants drag, drop, and configure in the theme editor — and the quality of those sections determines whether a store looks generic or genuinely professional.

Shopify's default Dawn theme ships with solid sections, but they only cover the basics. The moment a client asks for a dismissable announcement bar, a reversible image-with-text layout, or a curated product showcase with variant selectors, you need to build custom sections from scratch.

This shopify custom sections liquid tutorial walks you through building five production-ready sections, each with complete Liquid markup, JSON schema definitions, and CSS. These are not toy examples. They are sections you can drop into any Online Store 2.0 theme and ship to a live store today.

If you are new to Shopify's templating system, start with our Shopify theme development for beginners guide first. It covers the CLI setup, file structure, and Liquid fundamentals you will need to follow along here.

For context on how sections fit within the broader architecture, Shopify's official section documentation is the canonical reference.

How Shopify Sections Work (Quick Refresher)

Before we start building, here is a 60-second refresher on how sections function in Online Store 2.0.

A section is a single Liquid file in your theme's sections/ directory. Every section has two parts:

  1. Liquid markup — the HTML, Liquid tags, and logic that render on the page.
  2. Schema — a JSON block at the bottom of the file that defines the section's settings, blocks, and presets.

The schema is what makes your section configurable in the theme editor. When a merchant clicks on your section in the customizer, the settings you define in the schema appear as form fields — text inputs, image pickers, color selectors, range sliders, and more.

Here is the minimal skeleton every section follows:

liquidliquid
<section class="my-section">
  <!-- Liquid markup goes here -->
  <h2>{{ section.settings.heading }}</h2>
</section>

{% schema %}
{
  "name": "My Section",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Hello World"
    }
  ],
  "presets": [
    {
      "name": "My Section"
    }
  ]
}
{% endschema %}

The presets array is what makes the section available in the "Add section" menu of the theme editor. Without a preset, the section exists but cannot be added by merchants.

With that foundation in place, let's build.

Section 1: Dismissable Announcement Bar

Glow-line holographic mockup of a dismissable announcement bar and its theme editor settings.

Announcement bars drive urgency — flash sales, free shipping thresholds, new product launches. A good one is visually distinct, editable from the theme customizer, and dismissable so returning visitors are not annoyed.

The Liquid Markup

Create a file at sections/announcement-bar-custom.liquid:

liquidliquid
{%- if section.settings.show_announcement -%}
<div
  id="announcement-bar-{{ section.id }}"
  class="announcement-bar"
  style="
    background-color: {{ section.settings.bg_color }};
    color: {{ section.settings.text_color }};
  "
  role="region"
  aria-label="Announcement"
  data-section-id="{{ section.id }}"
>
  <div class="announcement-bar__inner">
    {%- if section.settings.link != blank -%}
      <a href="{{ section.settings.link }}" class="announcement-bar__link">
        {{ section.settings.text }}
      </a>
    {%- else -%}
      <p class="announcement-bar__text">{{ section.settings.text }}</p>
    {%- endif -%}

    {%- if section.settings.dismissable -%}
      <button
        class="announcement-bar__close"
        aria-label="Dismiss announcement"
        data-dismiss-announcement="{{ section.id }}"
      >
        {% render 'icon-close' %}
      </button>
    {%- endif -%}
  </div>
</div>
{%- endif -%}

<style>
  .announcement-bar {
    position: relative;
    padding: 10px 40px 10px 16px;
    text-align: center;
    font-size: 14px;
    font-weight: 500;
    line-height: 1.4;
    transition: max-height 0.3s ease, opacity 0.3s ease;
    overflow: hidden;
  }

  .announcement-bar.is-hidden {
    max-height: 0;
    opacity: 0;
    padding: 0;
  }

  .announcement-bar__inner {
    max-width: 1200px;
    margin: 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
  }

  .announcement-bar__link {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 3px;
  }

  .announcement-bar__link:hover {
    text-decoration-thickness: 2px;
  }

  .announcement-bar__text {
    margin: 0;
  }

  .announcement-bar__close {
    position: absolute;
    right: 12px;
    top: 50%;
    transform: translateY(-50%);
    background: none;
    border: none;
    color: inherit;
    cursor: pointer;
    padding: 4px;
    line-height: 0;
  }

  .announcement-bar__close svg {
    width: 16px;
    height: 16px;
  }
</style>

<script>
  (function () {
    const bar = document.getElementById('announcement-bar-{{ section.id }}');
    if (!bar) return;

    const key = 'announcement-dismissed-{{ section.id }}';

    // Check if previously dismissed
    if (sessionStorage.getItem(key) === 'true') {
      bar.classList.add('is-hidden');
      return;
    }

    const closeBtn = bar.querySelector('[data-dismiss-announcement]');
    if (closeBtn) {
      closeBtn.addEventListener('click', function () {
        bar.classList.add('is-hidden');
        sessionStorage.setItem(key, 'true');
      });
    }
  })();
</script>

{% schema %}
{
  "name": "Announcement Bar (Custom)",
  "settings": [
    {
      "type": "checkbox",
      "id": "show_announcement",
      "label": "Show announcement",
      "default": true
    },
    {
      "type": "text",
      "id": "text",
      "label": "Announcement text",
      "default": "Free shipping on orders over $50 — Shop now"
    },
    {
      "type": "url",
      "id": "link",
      "label": "Link (optional)"
    },
    {
      "type": "checkbox",
      "id": "dismissable",
      "label": "Allow visitors to dismiss",
      "default": true
    },
    {
      "type": "color",
      "id": "bg_color",
      "label": "Background color",
      "default": "#1a1a1a"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text color",
      "default": "#ffffff"
    }
  ],
  "presets": [
    {
      "name": "Announcement Bar (Custom)"
    }
  ]
}
{% endschema %}

Key Design Decisions

The dismiss behavior uses sessionStorage rather than localStorage. This means the bar reappears on the next browser session, which is the right trade-off for promotional content — you want returning visitors to see updated announcements, but you don't want to nag them within a single visit.

The CSS transition on max-height and opacity ensures the bar collapses smoothly rather than snapping shut. The overflow: hidden prevents content flash during the animation.

Notice the aria-label on the section and the close button. Accessibility is not optional — screen readers need to understand what the region contains and how to interact with it. The Shopify theme accessibility guidelines cover this in detail.

Section 2: Image With Text (Reversible Split Layout)

The image-with-text section is perhaps the most versatile layout in e-commerce. Use it for brand storytelling, product highlights, testimonials, or feature explanations. Our version lets the merchant flip the layout direction and control the image-to-text ratio.

The Liquid Markup

Create sections/image-with-text-custom.liquid:

liquidliquid
{%- liquid
  assign layout_class = 'image-text--image-left'
  if section.settings.layout == 'text_first'
    assign layout_class = 'image-text--text-left'
  endif

  assign image_width = section.settings.image_width
  assign text_width = 100 | minus: image_width
-%}

<section
  class="image-text {{ layout_class }}"
  style="
    --image-width: {{ image_width }}%;
    --text-width: {{ text_width }}%;
    --section-padding-top: {{ section.settings.padding_top }}px;
    --section-padding-bottom: {{ section.settings.padding_bottom }}px;
    --text-bg: {{ section.settings.text_bg_color }};
    --text-color: {{ section.settings.text_color }};
  "
>
  <div class="image-text__grid">
    <div class="image-text__media">
      {%- if section.settings.image != blank -%}
        {{
          section.settings.image
          | image_url: width: 1200
          | image_tag:
            loading: 'lazy',
            class: 'image-text__img',
            widths: '375, 550, 750, 1000, 1200',
            sizes: '(max-width: 768px) 100vw, var(--image-width)'
        }}
      {%- else -%}
        {{ 'lifestyle-1' | placeholder_svg_tag: 'image-text__placeholder' }}
      {%- endif -%}
    </div>

    <div class="image-text__content">
      {%- if section.settings.subtitle != blank -%}
        <span class="image-text__subtitle">
          {{ section.settings.subtitle }}
        </span>
      {%- endif -%}

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

      {%- if section.settings.text != blank -%}
        <div class="image-text__body rte">{{ section.settings.text }}</div>
      {%- endif -%}

      {%- if section.settings.button_label != blank -%}
        <a
          href="{{ section.settings.button_link }}"
          class="image-text__btn btn btn--primary"
        >
          {{ section.settings.button_label }}
        </a>
      {%- endif -%}
    </div>
  </div>
</section>

<style>
  .image-text {
    padding-top: var(--section-padding-top);
    padding-bottom: var(--section-padding-bottom);
  }

  .image-text__grid {
    display: grid;
    grid-template-columns: var(--image-width) var(--text-width);
    gap: 0;
    max-width: 1400px;
    margin: 0 auto;
    align-items: center;
  }

  .image-text--text-left .image-text__grid {
    grid-template-columns: var(--text-width) var(--image-width);
  }

  .image-text--text-left .image-text__content {
    order: -1;
  }

  .image-text__media {
    overflow: hidden;
    line-height: 0;
  }

  .image-text__img,
  .image-text__placeholder {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .image-text__content {
    padding: 48px 56px;
    background-color: var(--text-bg);
    color: var(--text-color);
    display: flex;
    flex-direction: column;
    gap: 16px;
  }

  .image-text__subtitle {
    text-transform: uppercase;
    font-size: 12px;
    letter-spacing: 2px;
    font-weight: 600;
  }

  .image-text__heading {
    font-size: clamp(1.5rem, 3vw, 2.25rem);
    line-height: 1.2;
    margin: 0;
  }

  .image-text__body {
    font-size: 16px;
    line-height: 1.7;
  }

  .image-text__btn {
    align-self: flex-start;
    margin-top: 8px;
  }

  @media (max-width: 768px) {
    .image-text__grid,
    .image-text--text-left .image-text__grid {
      grid-template-columns: 1fr;
    }

    .image-text--text-left .image-text__content {
      order: 1;
    }

    .image-text__content {
      padding: 32px 20px;
    }
  }
</style>

{% schema %}
{
  "name": "Image With Text (Custom)",
  "settings": [
    {
      "type": "select",
      "id": "layout",
      "label": "Layout",
      "options": [
        { "value": "image_first", "label": "Image left, text right" },
        { "value": "text_first", "label": "Text left, image right" }
      ],
      "default": "image_first"
    },
    {
      "type": "range",
      "id": "image_width",
      "label": "Image width (%)",
      "min": 30,
      "max": 70,
      "step": 5,
      "default": 50,
      "unit": "%"
    },
    {
      "type": "image_picker",
      "id": "image",
      "label": "Image"
    },
    {
      "type": "text",
      "id": "subtitle",
      "label": "Subtitle",
      "default": "Our Story"
    },
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Built for merchants who care about quality"
    },
    {
      "type": "richtext",
      "id": "text",
      "label": "Body text",
      "default": "<p>Share your brand story, introduce a collection, or highlight what makes your products unique.</p>"
    },
    {
      "type": "text",
      "id": "button_label",
      "label": "Button label",
      "default": "Learn more"
    },
    {
      "type": "url",
      "id": "button_link",
      "label": "Button link"
    },
    {
      "type": "color",
      "id": "text_bg_color",
      "label": "Text background color",
      "default": "#f5f5f0"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text color",
      "default": "#1a1a1a"
    },
    {
      "type": "range",
      "id": "padding_top",
      "label": "Top padding",
      "min": 0,
      "max": 100,
      "step": 4,
      "default": 36,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "label": "Bottom padding",
      "min": 0,
      "max": 100,
      "step": 4,
      "default": 36,
      "unit": "px"
    }
  ],
  "presets": [
    {
      "name": "Image With Text (Custom)"
    }
  ]
}
{% endschema %}

How the Reversible Layout Works

The trick is CSS Grid with CSS custom properties. The --image-width and --text-width variables are computed in Liquid and injected as inline styles. When the merchant selects "Text left, image right," the grid columns swap and the content block gets order: -1 to visually reposition without changing the DOM order.

On mobile, the grid collapses to a single column regardless of the desktop layout choice. This avoids awkward narrow-column text on small screens.

The image_url filter with widths and sizes attributes generates a responsive srcset, so the browser downloads only the image size it actually needs. This is a significant performance win — a topic we cover in depth in our theme design best practices.

Section 3: Product Showcase With Variant Selector

Holographic UI showing a complex product variant selector wireframe with data connections on a dark background.

A featured product section lets merchants spotlight a single product anywhere on any page — not just the product page. This section pulls live product data, renders variant options, and includes an add-to-cart button.

The Liquid Markup

Create sections/product-showcase.liquid:

liquidliquid
{%- assign product = section.settings.product -%}

<section class="product-showcase" data-section-id="{{ section.id }}">
  <div class="product-showcase__inner">
    {%- if product != blank -%}
      <div class="product-showcase__grid">
        <!-- Image Gallery -->
        <div class="product-showcase__gallery">
          {%- for image in product.images limit: 4 -%}
            <div
              class="product-showcase__image-wrap{% if forloop.first %} is-active{% endif %}"
              data-image-index="{{ forloop.index0 }}"
            >
              {{
                image
                | image_url: width: 900
                | image_tag:
                  loading: 'lazy',
                  class: 'product-showcase__image',
                  widths: '375, 550, 750, 900'
              }}
            </div>
          {%- endfor -%}

          {%- if product.images.size > 1 -%}
            <div class="product-showcase__thumbs">
              {%- for image in product.images limit: 4 -%}
                <button
                  class="product-showcase__thumb{% if forloop.first %} is-active{% endif %}"
                  data-thumb-index="{{ forloop.index0 }}"
                  aria-label="View image {{ forloop.index }}"
                >
                  {{ image | image_url: width: 100 | image_tag }}
                </button>
              {%- endfor -%}
            </div>
          {%- endif -%}
        </div>

        <!-- Product Details -->
        <div class="product-showcase__details">
          {%- if section.settings.show_vendor and product.vendor != blank -%}
            <p class="product-showcase__vendor">{{ product.vendor }}</p>
          {%- endif -%}

          <h2 class="product-showcase__title">
            <a href="{{ product.url }}">{{ product.title }}</a>
          </h2>

          <div class="product-showcase__price">
            {%- if product.compare_at_price > product.price -%}
              <span class="product-showcase__compare-price">
                {{ product.compare_at_price | money }}
              </span>
            {%- endif -%}
            <span class="product-showcase__current-price">
              {{ product.price | money }}
            </span>
          </div>

          {%- if section.settings.show_description -%}
            <div class="product-showcase__description rte">
              {{ product.description | truncatewords: 40 }}
            </div>
          {%- endif -%}

          {%- unless product.has_only_default_variant -%}
            {%- for option in product.options_with_values -%}
              <div class="product-showcase__option">
                <label
                  class="product-showcase__option-label"
                  for="showcase-option-{{ section.id }}-{{ forloop.index0 }}"
                >
                  {{ option.name }}
                </label>
                <select
                  id="showcase-option-{{ section.id }}-{{ forloop.index0 }}"
                  class="product-showcase__select"
                  data-option-index="{{ forloop.index0 }}"
                >
                  {%- for value in option.values -%}
                    <option
                      value="{{ value }}"
                      {% if value == option.selected_value %}selected{% endif %}
                    >
                      {{ value }}
                    </option>
                  {%- endfor -%}
                </select>
              </div>
            {%- endfor -%}
          {%- endunless -%}

          <button
            type="button"
            class="product-showcase__atc btn btn--primary"
            data-product-id="{{ product.id }}"
            data-variant-id="{{ product.selected_or_first_available_variant.id }}"
            {% unless product.available %}disabled{% endunless %}
          >
            {%- if product.available -%}
              {{ section.settings.button_text | default: 'Add to Cart' }}
            {%- else -%}
              Sold Out
            {%- endif -%}
          </button>
        </div>
      </div>
    {%- else -%}
      <div class="product-showcase__empty">
        <p>Select a product in the theme editor to display here.</p>
      </div>
    {%- endif -%}
  </div>
</section>

<style>
  .product-showcase {
    padding: 60px 20px;
  }

  .product-showcase__inner {
    max-width: 1200px;
    margin: 0 auto;
  }

  .product-showcase__grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 48px;
    align-items: start;
  }

  .product-showcase__gallery {
    position: sticky;
    top: 20px;
  }

  .product-showcase__image-wrap {
    display: none;
    border-radius: 8px;
    overflow: hidden;
  }

  .product-showcase__image-wrap.is-active {
    display: block;
  }

  .product-showcase__image {
    width: 100%;
    height: auto;
  }

  .product-showcase__thumbs {
    display: flex;
    gap: 8px;
    margin-top: 12px;
  }

  .product-showcase__thumb {
    width: 64px;
    height: 64px;
    border: 2px solid transparent;
    border-radius: 4px;
    overflow: hidden;
    cursor: pointer;
    padding: 0;
    background: none;
    opacity: 0.6;
    transition: opacity 0.2s, border-color 0.2s;
  }

  .product-showcase__thumb.is-active {
    border-color: #1a1a1a;
    opacity: 1;
  }

  .product-showcase__thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .product-showcase__vendor {
    text-transform: uppercase;
    font-size: 12px;
    letter-spacing: 1.5px;
    color: #888;
    margin: 0 0 8px;
  }

  .product-showcase__title {
    font-size: clamp(1.5rem, 3vw, 2rem);
    margin: 0 0 12px;
    line-height: 1.2;
  }

  .product-showcase__title a {
    color: inherit;
    text-decoration: none;
  }

  .product-showcase__price {
    font-size: 20px;
    margin-bottom: 16px;
    display: flex;
    gap: 10px;
    align-items: center;
  }

  .product-showcase__compare-price {
    text-decoration: line-through;
    color: #999;
    font-size: 16px;
  }

  .product-showcase__description {
    font-size: 15px;
    line-height: 1.7;
    color: #555;
    margin-bottom: 20px;
  }

  .product-showcase__option {
    margin-bottom: 16px;
  }

  .product-showcase__option-label {
    display: block;
    font-size: 13px;
    font-weight: 600;
    margin-bottom: 6px;
  }

  .product-showcase__select {
    width: 100%;
    padding: 10px 14px;
    border: 1px solid #d4d4d4;
    border-radius: 6px;
    font-size: 15px;
    background: #fff;
    appearance: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23333' fill='none'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 14px center;
  }

  .product-showcase__atc {
    width: 100%;
    padding: 16px 24px;
    font-size: 16px;
    font-weight: 600;
    margin-top: 8px;
  }

  .product-showcase__atc:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  .product-showcase__empty {
    text-align: center;
    padding: 80px 20px;
    color: #888;
  }

  @media (max-width: 768px) {
    .product-showcase__grid {
      grid-template-columns: 1fr;
      gap: 24px;
    }

    .product-showcase__gallery {
      position: static;
    }
  }
</style>

<script>
  (function () {
    const section = document.querySelector(
      '[data-section-id="{{ section.id }}"]'
    );
    if (!section) return;

    // Thumbnail gallery switching
    const thumbs = section.querySelectorAll('[data-thumb-index]');
    const images = section.querySelectorAll('[data-image-index]');

    thumbs.forEach((thumb) => {
      thumb.addEventListener('click', () => {
        const idx = thumb.dataset.thumbIndex;
        images.forEach((img) => img.classList.remove('is-active'));
        thumbs.forEach((t) => t.classList.remove('is-active'));
        section
          .querySelector(`[data-image-index="${idx}"]`)
          ?.classList.add('is-active');
        thumb.classList.add('is-active');
      });
    });

    // Variant selection + Ajax add-to-cart
    const selects = section.querySelectorAll('.product-showcase__select');
    const atcBtn = section.querySelector('.product-showcase__atc');
    const variants = {{ product.variants | json }};

    selects.forEach((select) => {
      select.addEventListener('change', () => {
        const selectedOptions = Array.from(selects).map((s) => s.value);
        const match = variants.find((v) =>
          v.options.every((opt, i) => opt === selectedOptions[i])
        );

        if (match && atcBtn) {
          atcBtn.dataset.variantId = match.id;
          atcBtn.disabled = !match.available;
          atcBtn.textContent = match.available ? 'Add to Cart' : 'Sold Out';
        }
      });
    });

    if (atcBtn) {
      atcBtn.addEventListener('click', () => {
        const variantId = atcBtn.dataset.variantId;
        if (!variantId) return;

        atcBtn.disabled = true;
        atcBtn.textContent = 'Adding...';

        fetch('/cart/add.js', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ items: [{ id: parseInt(variantId), quantity: 1 }] }),
        })
          .then((res) => res.json())
          .then(() => {
            atcBtn.textContent = 'Added!';
            setTimeout(() => {
              atcBtn.textContent = 'Add to Cart';
              atcBtn.disabled = false;
            }, 1500);
          })
          .catch(() => {
            atcBtn.textContent = 'Error — try again';
            atcBtn.disabled = false;
          });
      });
    }
  })();
</script>

{% schema %}
{
  "name": "Product Showcase",
  "settings": [
    {
      "type": "product",
      "id": "product",
      "label": "Product"
    },
    {
      "type": "checkbox",
      "id": "show_vendor",
      "label": "Show vendor",
      "default": true
    },
    {
      "type": "checkbox",
      "id": "show_description",
      "label": "Show description",
      "default": true
    },
    {
      "type": "text",
      "id": "button_text",
      "label": "Button text",
      "default": "Add to Cart"
    }
  ],
  "presets": [
    {
      "name": "Product Showcase"
    }
  ]
}
{% endschema %}

How the Variant Selector Works

The product's variants are serialized to JSON with {{ product.variants | json }} and stored in a JavaScript variable. When a merchant changes any option dropdown, the script iterates through the variant array, matches by option values, and updates the button's data-variant-id attribute. The add-to-cart button POSTs to Shopify's /cart/add.js endpoint — no page reload needed.

This is the same Ajax cart pattern used by Dawn and most professional Shopify themes. For a deeper dive into how Shopify's Ajax Cart API works, GemPages' section-building guide covers the integration patterns well.

Section 4: Newsletter Signup With Shopify Customer API

Email capture is a conversion essential. This section creates a styled newsletter form that submits to Shopify's built-in customer creation endpoint, keeping subscribers inside your store's customer list where they can be segmented and exported to email platforms like Klaviyo or Mailchimp.

The Liquid Markup

Create sections/newsletter-custom.liquid:

liquidliquid
<section
  class="newsletter-section"
  style="
    background-color: {{ section.settings.bg_color }};
    color: {{ section.settings.text_color }};
    --accent-color: {{ section.settings.accent_color }};
    padding-top: {{ section.settings.padding_top }}px;
    padding-bottom: {{ section.settings.padding_bottom }}px;
  "
>
  <div class="newsletter-section__inner">
    {%- if section.settings.heading != blank -%}
      <h2 class="newsletter-section__heading">
        {{ section.settings.heading }}
      </h2>
    {%- endif -%}

    {%- if section.settings.subtext != blank -%}
      <p class="newsletter-section__subtext">{{ section.settings.subtext }}</p>
    {%- endif -%}

    {%- form 'customer', id: 'newsletter-form' -%}
      <input type="hidden" name="contact[tags]" value="newsletter" />
      <input
        type="hidden"
        name="contact[accepts_marketing]"
        value="true"
      />

      {%- if form.errors -%}
        <div class="newsletter-section__error" role="alert">
          {{ form.errors | default_errors }}
        </div>
      {%- endif -%}

      {%- if form.posted_successfully? -%}
        <div class="newsletter-section__success" role="status">
          {{ section.settings.success_message }}
        </div>
      {%- endif -%}

      <div class="newsletter-section__form-row">
        <label for="newsletter-email-{{ section.id }}" class="visually-hidden">
          Email address
        </label>
        <input
          id="newsletter-email-{{ section.id }}"
          type="email"
          name="contact[email]"
          class="newsletter-section__input"
          placeholder="{{ section.settings.placeholder }}"
          required
          autocomplete="email"
          aria-label="Email address"
          {% if form.posted_successfully? %}
            value=""
          {% endif %}
        />
        <button type="submit" class="newsletter-section__btn btn">
          {{ section.settings.button_label }}
        </button>
      </div>

      {%- if section.settings.show_disclaimer -%}
        <p class="newsletter-section__disclaimer">
          {{ section.settings.disclaimer_text }}
        </p>
      {%- endif -%}
    {%- endform -%}
  </div>
</section>

<style>
  .newsletter-section {
    text-align: center;
  }

  .newsletter-section__inner {
    max-width: 600px;
    margin: 0 auto;
    padding: 0 20px;
  }

  .newsletter-section__heading {
    font-size: clamp(1.5rem, 3vw, 2.25rem);
    margin: 0 0 12px;
    line-height: 1.2;
  }

  .newsletter-section__subtext {
    font-size: 16px;
    line-height: 1.6;
    opacity: 0.85;
    margin: 0 0 28px;
  }

  .newsletter-section__form-row {
    display: flex;
    gap: 0;
    max-width: 480px;
    margin: 0 auto;
  }

  .newsletter-section__input {
    flex: 1;
    padding: 14px 18px;
    font-size: 15px;
    border: 2px solid transparent;
    border-radius: 6px 0 0 6px;
    outline: none;
    transition: border-color 0.2s;
  }

  .newsletter-section__input:focus {
    border-color: var(--accent-color);
  }

  .newsletter-section__btn {
    padding: 14px 28px;
    font-size: 15px;
    font-weight: 600;
    background: var(--accent-color);
    color: #fff;
    border: none;
    border-radius: 0 6px 6px 0;
    cursor: pointer;
    white-space: nowrap;
    transition: opacity 0.2s;
  }

  .newsletter-section__btn:hover {
    opacity: 0.9;
  }

  .newsletter-section__disclaimer {
    font-size: 12px;
    opacity: 0.6;
    margin-top: 14px;
  }

  .newsletter-section__error {
    background: #fee2e2;
    color: #991b1b;
    padding: 10px 16px;
    border-radius: 6px;
    margin-bottom: 16px;
    font-size: 14px;
  }

  .newsletter-section__success {
    background: #dcfce7;
    color: #166534;
    padding: 10px 16px;
    border-radius: 6px;
    margin-bottom: 16px;
    font-size: 14px;
  }

  @media (max-width: 480px) {
    .newsletter-section__form-row {
      flex-direction: column;
      gap: 10px;
    }

    .newsletter-section__input,
    .newsletter-section__btn {
      border-radius: 6px;
      width: 100%;
    }
  }
</style>

{% schema %}
{
  "name": "Newsletter Signup (Custom)",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Stay in the loop"
    },
    {
      "type": "textarea",
      "id": "subtext",
      "label": "Subtext",
      "default": "Get new product drops, exclusive deals, and Shopify tips delivered weekly."
    },
    {
      "type": "text",
      "id": "placeholder",
      "label": "Input placeholder",
      "default": "Enter your email"
    },
    {
      "type": "text",
      "id": "button_label",
      "label": "Button label",
      "default": "Subscribe"
    },
    {
      "type": "text",
      "id": "success_message",
      "label": "Success message",
      "default": "Thanks for subscribing! Check your inbox soon."
    },
    {
      "type": "checkbox",
      "id": "show_disclaimer",
      "label": "Show disclaimer",
      "default": true
    },
    {
      "type": "text",
      "id": "disclaimer_text",
      "label": "Disclaimer text",
      "default": "No spam, ever. Unsubscribe anytime."
    },
    {
      "type": "color",
      "id": "bg_color",
      "label": "Background color",
      "default": "#1a1a1a"
    },
    {
      "type": "color",
      "id": "text_color",
      "label": "Text color",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "accent_color",
      "label": "Button color",
      "default": "#95BF47"
    },
    {
      "type": "range",
      "id": "padding_top",
      "label": "Top padding",
      "min": 20,
      "max": 120,
      "step": 4,
      "default": 72,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "label": "Bottom padding",
      "min": 20,
      "max": 120,
      "step": 4,
      "default": 72,
      "unit": "px"
    }
  ],
  "presets": [
    {
      "name": "Newsletter Signup (Custom)"
    }
  ]
}
{% endschema %}

How the Shopify Customer Form Works

The {% form 'customer' %} tag generates a form that POSTs to Shopify's customer creation endpoint. The hidden contact[tags] field automatically tags the customer with "newsletter," which makes filtering easy in the Shopify admin and in email marketing integrations.

The hidden contact[accepts_marketing] field opts the customer into marketing emails — this is critical for GDPR and CAN-SPAM compliance. If your store serves EU customers, consider adding an explicit checkbox instead of a hidden field and making it required before submission.

The form.posted_successfully? and form.errors objects are Liquid's built-in form state management. After a successful submission, the success message appears. If the email is already registered, Shopify returns an error that default_errors renders automatically.

For a comprehensive comparison of how different platforms handle forms and customer data, our Shopify vs WooCommerce breakdown covers the architectural differences.

Section 5: Social Media / Instagram-Style Grid

Holographic grid visualization showing how external social data populates a custom section loop.

An Instagram-style grid brings social proof directly into the storefront. This version uses Shopify's metafield-friendly approach — the merchant uploads images via the theme editor rather than relying on fragile third-party API integrations that break when Instagram changes their authentication flow.

The Liquid Markup

Create sections/social-grid.liquid:

liquidliquid
<section
  class="social-grid"
  style="
    padding-top: {{ section.settings.padding_top }}px;
    padding-bottom: {{ section.settings.padding_bottom }}px;
  "
>
  {%- if section.settings.heading != blank -%}
    <div class="social-grid__header">
      <h2 class="social-grid__heading">{{ section.settings.heading }}</h2>

      {%- if section.settings.handle != blank -%}
        <a
          href="https://instagram.com/{{ section.settings.handle }}"
          class="social-grid__handle"
          target="_blank"
          rel="noopener"
        >
          @{{ section.settings.handle }}
        </a>
      {%- endif -%}
    </div>
  {%- endif -%}

  <div class="social-grid__grid social-grid__grid--{{ section.settings.columns }}">
    {%- for block in section.blocks -%}
      <a
        href="{{ block.settings.link | default: '#' }}"
        class="social-grid__item"
        {% if block.settings.link != blank %}
          target="_blank" rel="noopener"
        {% endif %}
        {{ block.shopify_attributes }}
      >
        {%- if block.settings.image != blank -%}
          {{
            block.settings.image
            | image_url: width: 600
            | image_tag:
              loading: 'lazy',
              class: 'social-grid__img',
              widths: '300, 400, 600',
              sizes: '(max-width: 768px) 50vw, 20vw'
          }}
        {%- else -%}
          {{ 'product-1' | placeholder_svg_tag: 'social-grid__placeholder' }}
        {%- endif -%}

        <div class="social-grid__overlay">
          {%- if block.settings.caption != blank -%}
            <p class="social-grid__caption">{{ block.settings.caption }}</p>
          {%- endif -%}
        </div>
      </a>
    {%- endfor -%}
  </div>

  {%- if section.settings.cta_label != blank -%}
    <div class="social-grid__cta-wrap">
      <a
        href="{{ section.settings.cta_link | default: '#' }}"
        class="social-grid__cta btn btn--outline"
        target="_blank"
        rel="noopener"
      >
        {{ section.settings.cta_label }}
      </a>
    </div>
  {%- endif -%}
</section>

<style>
  .social-grid {
    overflow: hidden;
  }

  .social-grid__header {
    text-align: center;
    margin-bottom: 32px;
    padding: 0 20px;
  }

  .social-grid__heading {
    font-size: clamp(1.5rem, 3vw, 2rem);
    margin: 0 0 8px;
  }

  .social-grid__handle {
    font-size: 16px;
    color: #888;
    text-decoration: none;
  }

  .social-grid__handle:hover {
    color: #1a1a1a;
  }

  .social-grid__grid {
    display: grid;
    gap: 4px;
  }

  .social-grid__grid--3 {
    grid-template-columns: repeat(3, 1fr);
  }

  .social-grid__grid--4 {
    grid-template-columns: repeat(4, 1fr);
  }

  .social-grid__grid--5 {
    grid-template-columns: repeat(5, 1fr);
  }

  .social-grid__grid--6 {
    grid-template-columns: repeat(6, 1fr);
  }

  .social-grid__item {
    position: relative;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    display: block;
  }

  .social-grid__img,
  .social-grid__placeholder {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.4s ease;
  }

  .social-grid__item:hover .social-grid__img {
    transform: scale(1.05);
  }

  .social-grid__overlay {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    transition: opacity 0.3s ease;
    padding: 16px;
  }

  .social-grid__item:hover .social-grid__overlay {
    opacity: 1;
  }

  .social-grid__caption {
    color: #fff;
    font-size: 14px;
    text-align: center;
    margin: 0;
  }

  .social-grid__cta-wrap {
    text-align: center;
    margin-top: 32px;
    padding: 0 20px;
  }

  @media (max-width: 768px) {
    .social-grid__grid--4,
    .social-grid__grid--5,
    .social-grid__grid--6 {
      grid-template-columns: repeat(3, 1fr);
    }
  }

  @media (max-width: 480px) {
    .social-grid__grid {
      grid-template-columns: repeat(2, 1fr) !important;
    }
  }
</style>

{% schema %}
{
  "name": "Social Grid",
  "settings": [
    {
      "type": "text",
      "id": "heading",
      "label": "Heading",
      "default": "Follow us on Instagram"
    },
    {
      "type": "text",
      "id": "handle",
      "label": "Instagram handle (without @)"
    },
    {
      "type": "select",
      "id": "columns",
      "label": "Columns (desktop)",
      "options": [
        { "value": "3", "label": "3" },
        { "value": "4", "label": "4" },
        { "value": "5", "label": "5" },
        { "value": "6", "label": "6" }
      ],
      "default": "4"
    },
    {
      "type": "text",
      "id": "cta_label",
      "label": "CTA button label",
      "default": "Follow @yourhandle"
    },
    {
      "type": "url",
      "id": "cta_link",
      "label": "CTA button link"
    },
    {
      "type": "range",
      "id": "padding_top",
      "label": "Top padding",
      "min": 0,
      "max": 100,
      "step": 4,
      "default": 48,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "label": "Bottom padding",
      "min": 0,
      "max": 100,
      "step": 4,
      "default": 48,
      "unit": "px"
    }
  ],
  "blocks": [
    {
      "type": "image",
      "name": "Image",
      "settings": [
        {
          "type": "image_picker",
          "id": "image",
          "label": "Image"
        },
        {
          "type": "url",
          "id": "link",
          "label": "Link"
        },
        {
          "type": "text",
          "id": "caption",
          "label": "Hover caption"
        }
      ]
    }
  ],
  "presets": [
    {
      "name": "Social Grid",
      "blocks": [
        { "type": "image" },
        { "type": "image" },
        { "type": "image" },
        { "type": "image" }
      ]
    }
  ]
}
{% endschema %}

Why Blocks Instead of a Single Image List

This section uses Shopify's block system rather than a fixed set of image settings. Each grid item is an independent block, which means merchants can add, remove, and reorder images from the theme editor. The max_blocks is not set, so merchants can add as many images as they need.

Each block has its own image, link, and hover caption — giving the merchant full control without touching code. The preset includes four placeholder blocks so the section looks populated immediately after adding it.

The TechBuzzOnline guide to Shopify custom sections provides additional context on how blocks interact with the theme editor's drag-and-drop interface.

Comparing All Five Sections at a Glance

Here is a quick reference table for each section, its complexity, and the Shopify features it uses:

SectionFile NameSchema SettingsUses BlocksJavaScriptDifficulty
Announcement Barannouncement-bar-custom.liquid6NoYes (dismiss)Beginner
Image With Textimage-with-text-custom.liquid12NoNoBeginner
Product Showcaseproduct-showcase.liquid4NoYes (variants + ATC)Intermediate
Newsletter Signupnewsletter-custom.liquid12NoNoBeginner
Social Gridsocial-grid.liquid7 + block settingsYesNoBeginner

The product showcase is the only section that qualifies as intermediate, and that is entirely because of the JavaScript variant matching and Ajax add-to-cart logic. The Liquid and schema portions are straightforward.

Testing and Debugging Your Custom Sections

Holographic UI showing multi-device viewport testing and diagnostics for custom Shopify sections.

Building sections is half the work. The other half is making sure they behave correctly across devices, browsers, and theme editor states.

Use Shopify CLI for Live Reload

Run shopify theme dev during development. Every time you save a .liquid file, the browser refreshes automatically. This feedback loop is essential — you will catch layout bugs, typos in setting IDs, and missing schema fields within seconds.

bashbash
shopify theme dev --store your-store.myshopify.com

Validate in the Theme Editor

After building each section, open the theme customizer and test every setting:

  • Text fields: Enter long strings, empty strings, and strings with special characters.
  • Color pickers: Confirm the color actually applies to the right element.
  • Range sliders: Move to both extremes. Does the section still look acceptable at 0px padding? At 100px?
  • Image pickers: Test with no image selected (the placeholder should render), portrait images, landscape images, and very large files.
  • Product pickers: Select a product with 1 variant, a product with 3 options, and a sold-out product.

Mobile Testing

Use Chrome DevTools device mode for quick checks, but always verify on a real device before shipping. Common issues:

  • Grid columns that look great on desktop but create 40px-wide columns on mobile.
  • Sticky positioning (position: sticky) on the product gallery behaving differently on iOS Safari.
  • Touch targets on the announcement bar close button being too small (minimum 44x44px per WCAG).

Common Debugging Patterns

When a section does not appear in the theme editor "Add section" menu, the cause is almost always a missing or malformed presets array. Check for:

  • Trailing commas after the last item in any JSON array (invalid JSON).
  • Typos in setting id values that break Liquid references.
  • Missing closing {% endschema %} tags.

The Shopify CLI will surface Liquid syntax errors in the terminal, but schema JSON errors are sometimes silently swallowed. If something seems wrong, validate your schema JSON through jsonlint.com to catch formatting issues.

For a broader look at how section architecture connects to page speed and core web vitals, Viha Digital Commerce's guide to Shopify theme sections covers performance considerations specific to section-heavy themes.

Performance Tips for Production Sections

Custom sections ship to every visitor. A few habits keep your sections fast.

Lazy-Load Everything Below the Fold

Every image_tag in our examples uses loading: 'lazy'. For the announcement bar or hero sections that appear above the fold, switch to loading: 'eager' — but for everything else, lazy loading prevents unnecessary image downloads on initial page load.

Scope Your CSS

Each section's styles are contained within a <style> block inside the section file itself. This is intentional. Shopify renders each section independently, and scoping styles to the section avoids global CSS conflicts.

If your sections share common utility classes (.btn, .rte, .visually-hidden), define those in your theme's base.css or global.css rather than duplicating them in every section file.

Minimize Inline JavaScript

The announcement bar and product showcase sections include <script> blocks because they need interactivity. But keep these scripts minimal. Heavy JavaScript belongs in your theme's bundled theme.js file where it can be deferred and cached.

For the product showcase specifically, consider moving the variant selection logic into your main JavaScript file and dispatching custom events. This pattern is covered in Shopify's Ajax API documentation and scales better when you have multiple product sections on the same page.

Use CSS Custom Properties for Theming

All five sections use CSS custom properties (--image-width, --accent-color, --section-padding-top) rather than generating static CSS from Liquid. This keeps the CSS cacheable while still allowing per-instance configuration through inline style attributes on the section wrapper.

Extending These Sections With Blocks and App Blocks

Holographic schema diagram showing connections between sections, blocks, and app block extensions.

The five sections we built are self-contained, but the real power of Online Store 2.0 is composability. Here is how to take them further.

Adding Blocks to the Image With Text Section

You could convert the static heading/text/button into a block-based system where merchants add multiple content blocks — headings, text blocks, buttons, icons, spacers — and reorder them freely. The schema change looks like this:

jsonjson
"blocks": [
  {
    "type": "heading",
    "name": "Heading",
    "settings": [
      { "type": "text", "id": "heading", "label": "Heading" }
    ]
  },
  {
    "type": "text",
    "name": "Text",
    "settings": [
      { "type": "richtext", "id": "text", "label": "Text" }
    ]
  },
  {
    "type": "button",
    "name": "Button",
    "settings": [
      { "type": "text", "id": "label", "label": "Label" },
      { "type": "url", "id": "link", "label": "Link" }
    ]
  }
]

Then in the Liquid, you loop through section.blocks instead of referencing individual settings. This pattern is what separates a good Shopify developer from a great one — it gives merchants the flexibility to build layouts you never anticipated.

App Block Integration

Shopify app blocks let third-party apps inject content into your sections. If you want your product showcase to support reviews from Judge.me or Loox, add an app block type to your schema. The app's content renders inside your section without any code changes on your part.

For a deeper exploration of how app blocks differ from traditional theme sections, our guide to Shopify app blocks vs theme sections breaks down the architecture.

Putting It All Together: Your Section Development Workflow

After building these five sections, you have a repeatable workflow for any custom section a client requests:

  1. Start with the schema. Define what the merchant needs to control. This forces you to think about the section from the merchant's perspective before writing any HTML.
  2. Write the Liquid markup. Reference section.settings and block.settings to inject the configurable values.
  3. Add scoped CSS. Use CSS custom properties for values that change per-instance. Keep the styles inside the section file.
  4. Add JavaScript only when necessary. Dismiss buttons, variant selectors, and carousels need JS. Static content sections do not.
  5. Test in the theme editor. Every setting, every edge case, every device size.

This workflow scales from simple text banners to complex interactive sections. The five sections in this tutorial cover the most common patterns you will encounter: static content, media layouts, product data, form submissions, and block-based grids.

If you are building sections for clients and want to connect with other Shopify developers, the Talk Shop community is where builders share patterns, ask questions, and get feedback on their theme work. And if you are looking for experienced Shopify developers to collaborate with or hire, check out the experts network.

The best custom sections are the ones merchants actually enjoy using. Build with the theme editor in mind, test every setting, and ship with confidence. Everything you need to build production-quality Shopify sections is already in Liquid — you just have to know the patterns, and now you do.

Shopify DevelopmentTheme Design
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