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:
- Liquid markup — the HTML, Liquid tags, and logic that render on the page.
- 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:
<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

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:
{%- 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:
{%- 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

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:
{%- 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:
<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

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:
<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:
| Section | File Name | Schema Settings | Uses Blocks | JavaScript | Difficulty |
|---|---|---|---|---|---|
| Announcement Bar | announcement-bar-custom.liquid | 6 | No | Yes (dismiss) | Beginner |
| Image With Text | image-with-text-custom.liquid | 12 | No | No | Beginner |
| Product Showcase | product-showcase.liquid | 4 | No | Yes (variants + ATC) | Intermediate |
| Newsletter Signup | newsletter-custom.liquid | 12 | No | No | Beginner |
| Social Grid | social-grid.liquid | 7 + block settings | Yes | No | Beginner |
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

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.
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
idvalues 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

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:
"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:
- 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.
- Write the Liquid markup. Reference
section.settingsandblock.settingsto inject the configurable values. - Add scoped CSS. Use CSS custom properties for values that change per-instance. Keep the styles inside the section file.
- Add JavaScript only when necessary. Dismiss buttons, variant selectors, and carousels need JS. Static content sections do not.
- 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.

About Talk Shop
The Talk Shop team — insights from our community of Shopify developers, merchants, and experts.
Related Insights
The ecommerce newsletter that's actually useful.
Daily trends, teardowns, and tactics from the top 1% of ecommerce brands. Delivered every morning.
