Shopify Section Patterns
Section Anatomy
A complete Shopify section consists of:
-
Liquid markup - HTML structure with dynamic content
-
Inline CSS - {% stylesheet %} tag (optional)
-
Inline JavaScript - {% javascript %} tag (optional)
-
Schema - {% schema %} JSON configuration
Complete Section Template
{%- comment -%} Section: [Name] Description: [Brief description] Usage: Add via Theme Editor {%- endcomment -%}
{%- liquid assign heading = section.settings.heading | default: 'Default Heading' assign bg_color = section.settings.bg_color assign text_color = section.settings.text_color -%}
<section id="section-{{ section.id }}" class="my-section" data-section-id="{{ section.id }}"
<div class="container"> {%- if heading != blank -%} <h2>{{ heading | escape }}</h2> {%- endif -%}
{%- comment -%} Section content {%- endcomment -%}
</div> </section>
{% stylesheet %} #section-{{ section.id }} { padding: {{ section.settings.padding_top }}px 0 {{ section.settings.padding_bottom }}px; background-color: {{ bg_color }}; color: {{ text_color }}; }
.container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
@media (min-width: 768px) { /* Tablet styles */ }
@media (min-width: 1024px) { /* Desktop styles */ } {% endstylesheet %}
{% javascript %} (function() { 'use strict';
function initSection(sectionId) {
const section = document.querySelector('[data-section-id="' + sectionId + '"]');
if (!section) return;
// Your JavaScript here
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
initSection('{{ section.id }}');
});
// Re-initialize when section reloads in theme editor
document.addEventListener('shopify:section:load', function(event) {
if (event.detail.sectionId === '{{ section.id }}') {
initSection('{{ section.id }}');
}
});
})(); {% endjavascript %}
{% schema %} { "name": "Section Name", "tag": "section", "class": "section-wrapper", "settings": [ { "type": "text", "id": "heading", "label": "Heading", "default": "Default Heading" }, { "type": "color", "id": "bg_color", "label": "Background Color", "default": "#ffffff" }, { "type": "color", "id": "text_color", "label": "Text Color", "default": "#000000" }, { "type": "range", "id": "padding_top", "label": "Top Spacing", "min": 0, "max": 100, "step": 4, "unit": "px", "default": 40 }, { "type": "range", "id": "padding_bottom", "label": "Bottom Spacing", "min": 0, "max": 100, "step": 4, "unit": "px", "default": 40 } ], "presets": [ { "name": "Section Name" } ] } {% endschema %}
When to Use Inline CSS/JS
Use Inline {% stylesheet %} When:
-
CSS needs Liquid variables: {{ section.settings.color }}
-
Dynamic styles based on merchant settings
-
Section-specific styles that don't need external file
Use Inline {% javascript %} When:
-
JS needs Liquid settings: {{ section.settings.enable_feature }}
-
Section-specific functionality
-
Need access to {{ section.id }} or other Liquid values
Skip Inline Tags When:
-
Styles/scripts are static and reusable
-
Better to use theme.css or theme.js
-
No Liquid variables needed
Common Section Patterns
- Product Grid Section
{%- liquid assign collection = section.settings.collection assign products_count = section.settings.products_count assign columns = section.settings.columns_desktop -%}
<div class="product-grid"> {%- for product in collection.products limit: products_count -%} <div class="product-item"> <a href="{{ product.url }}"> {%- if product.featured_image -%} <img src="{{ product.featured_image | image_url: width: 400 }}" alt="{{ product.featured_image.alt | escape }}" loading="lazy" > {%- endif -%}
<h3>{{ product.title | escape }}</h3>
<p>{{ product.price | money }}</p>
</a>
</div>
{%- endfor -%} </div>
{% stylesheet %} .product-grid { display: grid; gap: 1.5rem; grid-template-columns: 1fr; }
@media (min-width: 768px) { .product-grid { grid-template-columns: repeat({{ columns }}, 1fr); } } {% endstylesheet %}
- Hero Banner Section
{%- liquid assign image = section.settings.image assign heading = section.settings.heading assign text = section.settings.text assign button_label = section.settings.button_label assign button_link = section.settings.button_link -%}
<div class="hero-banner"> {%- if image -%} <div class="hero-banner__image"> <img src="{{ image | image_url: width: 1600 }}" alt="{{ image.alt | escape }}" > </div> {%- endif -%}
<div class="hero-banner__content"> {%- if heading != blank -%} <h1>{{ heading | escape }}</h1> {%- endif -%}
{%- if text != blank -%}
<p>{{ text }}</p>
{%- endif -%}
{%- if button_label != blank and button_link != blank -%}
<a href="{{ button_link }}" class="button">
{{ button_label | escape }}
</a>
{%- endif -%}
</div> </div>
{% stylesheet %} .hero-banner { position: relative; min-height: 500px; display: flex; align-items: center; justify-content: center; text-align: center; }
.hero-banner__image { position: absolute; inset: 0; z-index: -1; }
.hero-banner__image img { width: 100%; height: 100%; object-fit: cover; }
.hero-banner__content { padding: 2rem; background: rgba(255, 255, 255, 0.9); border-radius: 8px; }
.button { display: inline-block; padding: 1rem 2rem; background: #000; color: #fff; text-decoration: none; border-radius: 4px; margin-top: 1rem; } {% endstylesheet %}
- Testimonials Section with Blocks
<div class="testimonials"> {%- for block in section.blocks -%} <div class="testimonial" {{ block.shopify_attributes }}> {%- if block.settings.quote != blank -%} <blockquote> {{ block.settings.quote }} </blockquote> {%- endif -%}
{%- if block.settings.author != blank -%}
<cite>{{ block.settings.author | escape }}</cite>
{%- endif -%}
</div>
{%- endfor -%} </div>
{% schema %} { "name": "Testimonials", "blocks": [ { "type": "testimonial", "name": "Testimonial", "settings": [ { "type": "textarea", "id": "quote", "label": "Quote" }, { "type": "text", "id": "author", "label": "Author" } ] } ], "presets": [ { "name": "Testimonials", "blocks": [ { "type": "testimonial" } ] } ] } {% endschema %}
Shopify Section Events
Handle theme editor events:
{% javascript %} (function() { 'use strict';
function initSection(sectionId) {
const section = document.querySelector('[data-section-id="' + sectionId + '"]');
if (!section) return;
console.log('Section initialized:', sectionId);
}
function cleanupSection(sectionId) {
// Clean up event listeners, timers, etc.
console.log('Section cleaned up:', sectionId);
}
// Load event - section added or page loaded
document.addEventListener('shopify:section:load', function(event) {
initSection(event.detail.sectionId);
});
// Unload event - section removed
document.addEventListener('shopify:section:unload', function(event) {
cleanupSection(event.detail.sectionId);
});
// Select event - section selected in theme editor
document.addEventListener('shopify:section:select', function(event) {
console.log('Section selected:', event.detail.sectionId);
});
// Deselect event - section deselected in theme editor
document.addEventListener('shopify:section:deselect', function(event) {
console.log('Section deselected:', event.detail.sectionId);
});
// Block select event - block selected in theme editor
document.addEventListener('shopify:block:select', function(event) {
console.log('Block selected:', event.detail.blockId);
});
// Block deselect event - block deselected in theme editor
document.addEventListener('shopify:block:deselect', function(event) {
console.log('Block deselected:', event.detail.blockId);
});
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
initSection('{{ section.id }}');
});
})(); {% endjavascript %}
Best Practices
- Use Section ID for Unique Styling
<div id="section-{{ section.id }}" class="my-section"> <!-- Content --> </div>
{% stylesheet %} #section-{{ section.id }} { /* Section-specific styles using Liquid variables */ background: {{ section.settings.bg_color }}; }
.my-section { /* General styles without Liquid variables */ padding: 2rem 0; } {% endstylesheet %}
- Provide Sensible Defaults
{%- liquid assign heading = section.settings.heading | default: 'Default Heading' assign columns = section.settings.columns | default: 3 assign show_prices = section.settings.show_prices | default: true -%}
- Handle Empty States
{%- if collection.products.size > 0 -%} <!-- Show products --> {%- else -%} <p>No products available in this collection.</p> {%- endif -%}
- Mobile-First Responsive
/* Mobile first - base styles */ .grid { grid-template-columns: 1fr; }
/* Tablet and up */ @media (min-width: 768px) { .grid { grid-template-columns: repeat(2, 1fr); } }
/* Desktop and up */ @media (min-width: 1024px) { .grid { grid-template-columns: repeat(3, 1fr); } }
- Use data- Attributes for JavaScript
<div class="slider" data-autoplay="{{ section.settings.autoplay }}" data-speed="{{ section.settings.speed }}"
<!-- Slider content --> </div>
{% javascript %} const slider = document.querySelector('.slider'); const autoplay = slider.dataset.autoplay === 'true'; const speed = parseInt(slider.dataset.speed, 10); {% endjavascript %}
Common Patterns
Conditional Classes
<div class="card{% if product.available %} in-stock{% else %} sold-out{% endif %}"> <!-- Card content --> </div>
Dynamic Grid Columns
{% stylesheet %} .product-grid { display: grid; gap: 1.5rem; grid-template-columns: repeat({{ section.settings.columns_mobile }}, 1fr); }
@media (min-width: 1024px) { .product-grid { grid-template-columns: repeat({{ section.settings.columns_desktop }}, 1fr); } } {% endstylesheet %}
Loading States
{% javascript %} function addToCart(button) { button.classList.add('loading'); button.disabled = true;
// Add to cart logic...
button.classList.remove('loading');
button.disabled = false;
} {% endjavascript %}
Create sections that are flexible, performant, and easy for merchants to customize through the theme editor.