Shopify Custom Liquid Theme Development
Custom theme — when no Theme Store theme fits by design, structure, or performance. Development in Liquid — Shopify's template language — with CSS (usually Tailwind or BEM) and Vanilla JS / Alpine.js.
Online Store 2.0 — Mandatory Standard
All modern themes built on OS 2.0 architecture. Key differences from legacy themes:
-
JSON templates instead of Liquid for pages (
templates/product.json,templates/collection.json) - Sections Everywhere — sections can be added to any page, not just homepage
- Blocks inside sections — nested structure with Theme Editor editing
- app blocks — apps embed via official App Extension, not hardcoded
Theme file structure:
theme/
├── assets/ # CSS, JS, images, fonts
├── config/ # settings_schema.json, settings_data.json
├── layout/ # theme.liquid, password.liquid
├── locales/ # translation strings
├── sections/ # .liquid files
├── snippets/ # reusable fragments
└── templates/ # .json page templates
Section — Main Unit
Section is .liquid file in /sections/ with mandatory {% schema %} block:
{%- comment -%} sections/product-hero.liquid {%- endcomment -%}
<div class="product-hero" style="--accent: {{ section.settings.accent_color }}">
{% for block in section.blocks %}
{% case block.type %}
{% when 'title' %}
<h1 class="product-hero__title">{{ product.title }}</h1>
{% when 'price' %}
<div class="product-hero__price">
{{ product.selected_or_first_available_variant.price | money }}
</div>
{% when 'add_to_cart' %}
<button
type="submit"
form="product-form-{{ section.id }}"
class="btn btn--primary"
{% unless product.available %}disabled{% endunless %}
>
{{ block.settings.button_text | default: 'Add to Cart' }}
</button>
{% endcase %}
{% endfor %}
</div>
{% schema %}
{
"name": "Product Hero",
"tag": "section",
"class": "section-product-hero",
"settings": [
{
"type": "color",
"id": "accent_color",
"label": "Accent Color",
"default": "#0066cc"
}
],
"blocks": [
{ "type": "title", "name": "Product Title", "limit": 1 },
{
"type": "price",
"name": "Price",
"limit": 1
},
{
"type": "add_to_cart",
"name": "Add to Cart Button",
"limit": 1,
"settings": [
{
"type": "text",
"id": "button_text",
"label": "Button Text",
"default": "Add to Cart"
}
]
}
],
"presets": [
{
"name": "Product Hero",
"blocks": [
{ "type": "title" },
{ "type": "price" },
{ "type": "add_to_cart" }
]
}
]
}
{% endschema %}
Working with Shopify Data in Liquid
Main objects available in templates:
{%- comment -%} Variant with URL parameter {%- endcomment -%}
{%- assign current_variant = product.selected_or_first_available_variant -%}
{%- comment -%} Collection with sorting {%- endcomment -%}
{%- assign sorted_products = collection.products | sort: 'price' -%}
{%- comment -%} Metafields {%- endcomment -%}
{%- assign delivery_days = product.metafields.custom.delivery_days.value -%}
{%- comment -%} Customer {%- endcomment -%}
{%- if customer -%}
Hello, {{ customer.first_name }}!
{%- endif -%}
JavaScript in Theme
Shopify doesn't mandate framework. Recommended 2024–2025 approach — Web Components or Alpine.js for interactivity without heavy bundle:
// assets/product-form.js
class ProductForm extends HTMLElement {
connectedCallback() {
this.form = this.querySelector('form');
this.submitButton = this.querySelector('[type="submit"]');
this.form.addEventListener('submit', this.onSubmit.bind(this));
}
async onSubmit(event) {
event.preventDefault();
this.submitButton.setAttribute('disabled', '');
const formData = new FormData(this.form);
const response = await fetch('/cart/add.js', {
method: 'POST',
body: formData
});
if (!response.ok) {
const error = await response.json();
console.error('Cart error:', error.description);
return;
}
document.dispatchEvent(new CustomEvent('cart:item-added', {
detail: await response.json()
}));
this.submitButton.removeAttribute('disabled');
}
}
customElements.define('product-form', ProductForm);
Theme Performance
Lighthouse Score — indirect ranking factor and direct conversion factor. Common custom theme issues and solutions:
Render-blocking scripts — all third-party scripts only with defer or async. Built-in scripts at end of </body> or via DOMContentLoaded.
Unoptimized images — Shopify CDN processes via URL parameters:
{{ product.featured_image
| image_url: width: 800
| image_tag:
loading: 'lazy',
widths: '400, 800, 1200',
sizes: '(max-width: 768px) 100vw, 50vw'
}}
Excess CSS — split critical and non-critical. Critical inline in <head>, non-critical with rel="preload" and onload.
Font loading — preconnect + font-display: swap:
{%- comment -%} In layout/theme.liquid <head> {%- endcomment -%}
<link rel="preconnect" href="https://fonts.shopifycdn.com" crossorigin>
Development Tools
# Shopify CLI 3.x
npm install -g @shopify/cli
# Run local server with hot-reload
shopify theme dev --store=mystore.myshopify.com --live-reload=hot-reload
# Push changes to staging theme
shopify theme push --theme=THEME_ID
# Create new theme project based on Dawn
shopify theme init my-custom-theme
CSS and JS build — usually Vite or webpack, results copied to /assets/. Shopify CLI doesn't replace bundler — just syncs files with store.
Testing
- Functional: cart, checkout, filters via browser
- Cross-browser: Chrome, Firefox, Safari (especially iOS Safari — specific bugs)
- Performance: Lighthouse in incognito, WebPageTest with 4G throttling
- Theme Check — Shopify linter:
shopify theme check
# Checks: deprecated filters, missing alt on images,
# incorrect asset links, translation issues
Timeline
Custom theme from scratch by ready design (up to 15 page types): 3–6 weeks. Customize existing theme to brand guide while preserving updateability: 1–2 weeks. Refactor legacy theme to OS 2.0: 2–3 weeks.







