Shopify Theme Development Guide — Build Custom Themes with Liquid
Shopify theme development is the process of building the front-end experience of a Shopify store using Liquid, HTML, CSS, and JavaScript. Whether you want to build a Shopify theme from scratch or customize an existing one, this guide covers everything — from setting up your development environment to deploying an optimized Online Store 2.0 theme. The Shopify code language at the core of every theme is Liquid, and mastering it is the key to building great storefronts.
What is Shopify Theme Development?
Shopify theme development means creating the templates, sections, and layouts that control how a Shopify store looks and feels. Every page a customer sees — the homepage, product pages, collection pages, cart, and checkout — is rendered by a theme. Themes are built with a combination of Liquid template code, HTML, CSS, and JavaScript.
Here's what makes Shopify theme development unique:
- Liquid-powered — The Shopify code language for themes is Liquid, a template language that connects your store's data (products, collections, customers) to the HTML that customers see in their browser.
- Server-side rendered — Shopify processes your Liquid templates on its servers and sends fully-rendered HTML to the browser. This means fast initial page loads and great SEO out of the box.
- Online Store 2.0 — The modern Shopify theme architecture introduced JSON templates and app blocks, giving merchants far more flexibility to customize their store without touching code.
- Section-based architecture — Themes are built from modular, reusable sections that merchants can add, remove, and rearrange using the theme editor.
<!-- A Shopify theme turns Liquid + HTML into a storefront -->
<!-- theme.liquid wraps every page -->
<html>
<head>
<title>{{ page_title }} | {{ shop.name }}</title>
{{ content_for_header }}
{{ 'theme.css' | asset_url | stylesheet_tag }}
</head>
<body>
{% section 'header' %}
<main>
{{ content_for_layout }}
</main>
{% section 'footer' %}
</body>
</html>Theme File Structure
Every Shopify theme follows a specific folder structure. Understanding this structure is the foundation of Shopify theme development. Here's what each directory does and what files you'll find inside.
layout/
Contains the master layout files that wrap every page in your store. The primary file is theme.liquid, which defines the <html>, <head>, and <body> structure. You can also create alternate layouts like password.liquid for the password page.
templates/
Page-level templates that control different page types. In Online Store 2.0, these are JSON files (product.json, collection.json, index.json) that define which sections appear on each page. Legacy themes use .liquid files instead.
sections/
Modular, reusable content blocks that merchants can customize through the theme editor. Sections are the building blocks of modern Shopify themes. Each section file contains both the Liquid/HTML markup and a JSON schema that defines its settings.
snippets/
Small, reusable pieces of code that can be included in templates, sections, and other snippets. Commonly used for product cards, icon SVGs, price displays, and other repeated patterns. You include them with the render tag.
assets/
CSS stylesheets, JavaScript files, images, and fonts. These aren't Liquid template files themselves, but they can be referenced from Liquid using the asset_url filter.
config/
JSON configuration files. settings_schema.json defines the global theme settings that appear in the theme editor, and settings_data.json stores the merchant's saved values.
locales/
Translation files for multi-language stores. The default file is en.default.json. Translations are accessed in Liquid with the t filter.
<!-- Complete Shopify theme file structure -->
theme/
├── layout/
│ └── theme.liquid <!-- Master wrapper for every page -->
├── templates/
│ ├── index.json <!-- Homepage template (OS 2.0) -->
│ ├── product.json <!-- Product page template -->
│ ├── collection.json <!-- Collection page template -->
│ ├── cart.json <!-- Cart page template -->
│ ├── blog.json <!-- Blog listing template -->
│ ├── article.json <!-- Blog post template -->
│ └── page.json <!-- Static page template -->
├── sections/
│ ├── header.liquid <!-- Site header section -->
│ ├── footer.liquid <!-- Site footer section -->
│ ├── featured-collection.liquid
│ └── product-template.liquid
├── snippets/
│ ├── product-card.liquid <!-- Reusable product card -->
│ ├── price.liquid <!-- Price display snippet -->
│ └── icon-cart.liquid <!-- Cart icon SVG -->
├── assets/
│ ├── theme.css
│ └── theme.js
├── config/
│ ├── settings_schema.json <!-- Theme editor settings -->
│ └── settings_data.json <!-- Saved setting values -->
└── locales/
└── en.default.json <!-- English translations -->Setting Up Your Development Environment
Before you can build a Shopify theme, you need the right tools. The Shopify CLI is the official command-line tool for theme development. It lets you create, preview, and deploy themes from your local machine.
Installing the Shopify CLI
The Shopify CLI requires Node.js (version 18 or later). Install it with npm, then authenticate with your Shopify Partner account or development store.
# Install Shopify CLI globally
npm install -g @shopify/cli @shopify/theme
# Authenticate with your store
shopify auth login --store your-store.myshopify.com
# Create a new theme from scratch
shopify theme init my-custom-theme
# Or pull an existing theme from your store
shopify theme pull --store your-store.myshopify.com
# Start a local development server with hot reload
shopify theme dev --store your-store.myshopify.comTheme Development Workflow
The shopify theme dev command starts a local development server that syncs your changes to a development theme on your store in real time. You'll see a local URL (usually http://127.0.0.1:9292) where you can preview changes instantly without deploying.
- Hot reload — Changes to Liquid, CSS, and JS files are synced automatically. The browser refreshes when files change.
- Theme Check — Shopify's linter for themes. Run
shopify theme checkto catch errors, performance issues, and deprecated patterns in your Liquid code. - Version control — Use Git to track changes to your theme files. This is essential for collaboration and rolling back changes.
- Deploy — When you're ready, push to your live store with
shopify theme push.
# Run Theme Check to find issues in your Liquid code
shopify theme check
# Push your theme to the store
shopify theme push
# Push only specific files
shopify theme push --only sections/header.liquid
# List all themes on the store
shopify theme list
# Open the theme editor in your browser
shopify theme openBuilding Your First Shopify Theme
Let's build a Shopify theme from scratch. We'll start with the essential files that every theme needs: the layout, a template, and a section. If you're new to Liquid syntax, review the basics first — you'll need to understand conditionals and loops to follow along.
Step 1: The Layout — theme.liquid
The layout file is the outer shell of every page. It contains the HTML boilerplate, the <head> tag with meta information and stylesheets, and the <body> where your page content is injected via {{ content_for_layout }}.
<!-- layout/theme.liquid -->
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page_title }}</title>
<meta name="description" content="{{ page_description | escape }}">
{{ content_for_header }} <!-- Required: Shopify analytics & scripts -->
{{ 'base.css' | asset_url | stylesheet_tag }}
</head>
<body>
{% section 'header' %}
<main id="main-content" role="main">
{{ content_for_layout }}
</main>
{% section 'footer' %}
</body>
</html>The {{ content_for_header }} tag is required — Shopify injects its analytics, scripts, and app code here. The {{ content_for_layout }} tag is where the current page's template content gets rendered.
Step 2: A JSON Template
In Online Store 2.0, templates are JSON files that define which sections appear on each page type. Here's a product page template that includes a main product section.
// templates/product.json
{
"sections": {
"main": {
"type": "product-template",
"settings": {}
},
"recommendations": {
"type": "product-recommendations",
"settings": {
"heading": "You may also like"
}
}
},
"order": ["main", "recommendations"]
}Step 3: A Product Section
Sections contain the actual Liquid and HTML markup plus a JSON schema that defines the section's settings. This is the core of building a Shopify theme — every piece of visible content lives in a section.
<!-- sections/product-template.liquid -->
<div class="product-page">
<div class="product-gallery">
{% for image in product.images %}
<img
src="{{ image | image_url: width: 600 }}"
alt="{{ image.alt | default: product.title | escape }}"
loading="{% if forloop.first %}eager{% else %}lazy{% endif %}"
width="600"
height="{{ image.height | times: 600 | divided_by: image.width }}"
>
{% endfor %}
</div>
<div class="product-info">
<h1>{{ product.title }}</h1>
<p class="price">{{ product.price | money }}</p>
{% form 'product', product %}
<select name="id">
{% for variant in product.variants %}
<option value="{{ variant.id }}"
{% unless variant.available %}disabled{% endunless %}>
{{ variant.title }} — {{ variant.price | money }}
</option>
{% endfor %}
</select>
<button type="submit" {% unless product.available %}disabled{% endunless %}>
{% if product.available %}Add to Cart{% else %}Sold Out{% endif %}
</button>
{% endform %}
<div class="description">
{{ product.description }}
</div>
</div>
</div>
{% schema %}
{
"name": "Product Template",
"tag": "section",
"class": "product-section"
}
{% endschema %}Notice the {% schema %} block at the bottom. This is where you define the section's name, settings, and blocks. The schema is a JSON object that the theme editor reads to build the customization UI for merchants. We'll cover schemas in detail below.
Online Store 2.0 & JSON Templates
Online Store 2.0 (OS 2.0) is Shopify's modern theme architecture. The biggest change is that templates are now JSON files instead of Liquid files. This means merchants can add, remove, and reorder sections on every page type — not just the homepage.
In a legacy theme, the product.liquid template contained all the HTML and Liquid directly. In an OS 2.0 theme, the product.json template simply lists which sections to render and in what order. The actual markup lives in the section files.
- Sections everywhere — Every page can have its own set of customizable sections, not just the homepage.
- App blocks — Third-party apps can inject content into sections without editing theme code.
- Metafields in settings — Section settings can reference product, variant, and collection metafields as dynamic sources.
- Multiple templates per type — You can create alternate templates like
product.alternate.jsonfor different product page layouts.
// templates/index.json — Homepage template with multiple sections
{
"sections": {
"slideshow": {
"type": "slideshow",
"blocks": {
"slide_1": {
"type": "slide",
"settings": {
"image": "shopify://shop_images/hero-banner.jpg",
"heading": "Welcome to Our Store",
"button_label": "Shop Now",
"button_link": "/collections/all"
}
},
"slide_2": {
"type": "slide",
"settings": {
"image": "shopify://shop_images/sale-banner.jpg",
"heading": "Summer Sale",
"button_label": "View Sale",
"button_link": "/collections/summer-sale"
}
}
},
"block_order": ["slide_1", "slide_2"]
},
"featured": {
"type": "featured-collection",
"settings": {
"collection": "best-sellers",
"products_to_show": 8
}
},
"newsletter": {
"type": "newsletter",
"settings": {
"heading": "Subscribe for updates"
}
}
},
"order": ["slideshow", "featured", "newsletter"]
}Each key in the sections object is an ID for that section instance. The type maps to a file in the sections/ folder (e.g., featured-collection maps to sections/featured-collection.liquid). The order array controls the rendering sequence.
Section Schema & Settings
The schema is the heart of Shopify's customization system. It defines the settings that appear in the theme editor, allowing merchants to configure sections without writing code. Every section includes a {% schema %} tag at the bottom that contains a JSON object with the section's metadata, settings, and block definitions.
Section Settings
Settings are the individual inputs that merchants use to configure a section. Shopify provides many setting types: text, textarea, image_picker, select, checkbox, range, color, url, and more. Each setting has an id that you reference in your Liquid with section.settings.ID.
<!-- sections/featured-collection.liquid -->
<section class="featured-collection">
{% if section.settings.heading != blank %}
<h2>{{ section.settings.heading }}</h2>
{% endif %}
{% assign collection = collections[section.settings.collection] %}
<div class="product-grid columns-{{ section.settings.columns }}">
{% for product in collection.products limit: section.settings.products_to_show %}
{% render 'product-card', product: product %}
{% endfor %}
</div>
</section>
{% schema %}
{
"name": "Featured Collection",
"settings": [
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "Featured Products"
},
{
"type": "collection",
"id": "collection",
"label": "Collection"
},
{
"type": "range",
"id": "products_to_show",
"min": 2,
"max": 12,
"step": 2,
"default": 4,
"label": "Products to show"
},
{
"type": "select",
"id": "columns",
"label": "Columns",
"options": [
{ "value": "2", "label": "2" },
{ "value": "3", "label": "3" },
{ "value": "4", "label": "4" }
],
"default": "4"
}
],
"presets": [
{
"name": "Featured Collection"
}
]
}
{% endschema %}The presets array is important — it makes the section available for merchants to add via the theme editor. Without a preset, the section can only be included statically in templates. For a deeper look at conditionals and loops used in section templates, see our dedicated guide.
Blocks
Blocks are repeatable, reorderable sub-components within a section. Merchants can add multiple blocks of different types and arrange them in any order. Blocks are accessed with section.blocks in Liquid.
<!-- sections/slideshow.liquid -->
<div class="slideshow">
{% for block in section.blocks %}
<div class="slide" {{ block.shopify_attributes }}>
{% case block.type %}
{% when 'image_slide' %}
<img
src="{{ block.settings.image | image_url: width: 1200 }}"
alt="{{ block.settings.image.alt | escape }}"
loading="{% if forloop.first %}eager{% else %}lazy{% endif %}"
>
{% if block.settings.heading != blank %}
<h2>{{ block.settings.heading }}</h2>
{% endif %}
{% when 'video_slide' %}
<video autoplay muted loop>
<source src="{{ block.settings.video_url }}" type="video/mp4">
</video>
{% endcase %}
</div>
{% endfor %}
</div>
{% schema %}
{
"name": "Slideshow",
"max_blocks": 6,
"blocks": [
{
"type": "image_slide",
"name": "Image Slide",
"settings": [
{
"type": "image_picker",
"id": "image",
"label": "Image"
},
{
"type": "text",
"id": "heading",
"label": "Heading"
}
]
},
{
"type": "video_slide",
"name": "Video Slide",
"settings": [
{
"type": "url",
"id": "video_url",
"label": "Video URL"
}
]
}
],
"presets": [
{
"name": "Slideshow",
"blocks": [
{ "type": "image_slide" }
]
}
]
}
{% endschema %}The block.shopify_attributes output is important for the theme editor — it adds data attributes that let Shopify highlight the correct block when a merchant clicks on it in the editor. Learn more about case/when statements and other control flow used in block rendering.
Global Theme Settings (settings_schema.json)
The config/settings_schema.json file defines global settings for the entire theme — things like brand colors, fonts, social media links, and favicon. These settings are accessible everywhere in your theme through the settings object.
// config/settings_schema.json (excerpt)
[
{
"name": "Colors",
"settings": [
{
"type": "color",
"id": "color_primary",
"label": "Primary color",
"default": "#3B82F6"
},
{
"type": "color",
"id": "color_background",
"label": "Background color",
"default": "#FFFFFF"
}
]
},
{
"name": "Typography",
"settings": [
{
"type": "font_picker",
"id": "font_heading",
"label": "Heading font",
"default": "helvetica_n7"
},
{
"type": "font_picker",
"id": "font_body",
"label": "Body font",
"default": "helvetica_n4"
}
]
}
]
<!-- Access global settings anywhere in your theme -->
<style>
:root {
--color-primary: {{ settings.color_primary }};
--color-bg: {{ settings.color_background }};
--font-heading: {{ settings.font_heading.family }}, {{ settings.font_heading.fallback_families }};
--font-body: {{ settings.font_body.family }}, {{ settings.font_body.fallback_families }};
}
</style>Theme Performance Best Practices
A fast-loading theme directly impacts conversion rates and SEO rankings. Shopify stores are already optimized at the infrastructure level, but theme code plays a major role in performance. Here are the most impactful optimizations you can make when building a Shopify theme.
Lazy Loading Images
Only the first images visible above the fold should load immediately (eager loading). Everything below the fold should use loading="lazy" to defer loading until the customer scrolls near them. Always include width and height attributes to prevent layout shifts.
<!-- Lazy load images below the fold -->
{% for product in collection.products %}
<img
src="{{ product.featured_image | image_url: width: 400 }}"
alt="{{ product.featured_image.alt | default: product.title | escape }}"
width="400"
height="{{ product.featured_image.height | times: 400 | divided_by: product.featured_image.width }}"
loading="{% if forloop.index <= 4 %}eager{% else %}lazy{% endif %}"
>
{% endfor %}
<!-- Use srcset for responsive images -->
<img
src="{{ product.featured_image | image_url: width: 600 }}"
srcset="
{{ product.featured_image | image_url: width: 300 }} 300w,
{{ product.featured_image | image_url: width: 600 }} 600w,
{{ product.featured_image | image_url: width: 900 }} 900w"
sizes="(max-width: 768px) 100vw, 50vw"
alt="{{ product.featured_image.alt | escape }}"
loading="lazy"
>Asset Optimization
Minimize render-blocking resources by deferring JavaScript and preloading critical assets. Use Shopify's built-in asset filters to load CSS and JS efficiently.
<!-- Preload critical CSS -->
<link rel="preload" href="{{ 'base.css' | asset_url }}" as="style">
{{ 'base.css' | asset_url | stylesheet_tag }}
<!-- Defer non-critical JavaScript -->
<script src="{{ 'theme.js' | asset_url }}" defer></script>
<!-- Preconnect to Shopify CDN -->
<link rel="preconnect" href="https://cdn.shopify.com" crossorigin>
<!-- Inline critical CSS for above-the-fold content -->
<style>
.header { display: flex; align-items: center; padding: 1rem; }
.hero { min-height: 60vh; display: grid; place-items: center; }
</style>Liquid Render Performance
Liquid code runs on Shopify's servers, and inefficient templates can slow down page generation. Here are best practices for keeping your Liquid template code fast. For more on structuring efficient loops and conditionals, see our if/else and loops guide.
- Use
renderinstead ofinclude— Therendertag creates an isolated scope, which is more efficient and prevents variable leakage between snippets. - Limit nested loops — Avoid looping through all products inside a loop through all collections. Use
limiton your for loops when you don't need every item. - Assign expensive lookups once — If you reference the same object property multiple times, assign it to a variable first.
- Minimize Liquid in CSS/JS — Avoid putting Liquid tags inside large CSS or JS files. Use CSS custom properties set in the layout instead.
<!-- BAD: Nested loops with no limit -->
{% for collection in collections %}
{% for product in collection.products %}
<!-- This iterates over ALL products in ALL collections -->
{{ product.title }}
{% endfor %}
{% endfor %}
<!-- GOOD: Use limits and render snippets -->
{% for collection in collections limit: 3 %}
<h2>{{ collection.title }}</h2>
{% for product in collection.products limit: 4 %}
{% render 'product-card', product: product %}
{% endfor %}
{% endfor %}
<!-- GOOD: Assign expensive lookups once -->
{% assign featured_image = product.featured_image %}
{% assign image_alt = featured_image.alt | default: product.title | escape %}
<img
src="{{ featured_image | image_url: width: 400 }}"
alt="{{ image_alt }}"
>
<!-- GOOD: Use CSS custom properties instead of Liquid in CSS -->
<!-- In theme.liquid: -->
<style>
:root { --accent: {{ settings.color_primary }}; }
</style>
<!-- In base.css (no Liquid needed): -->
<!-- .btn { background: var(--accent); } -->Next Steps
You now have a comprehensive understanding of how to build a Shopify theme — from setting up your environment and understanding the file structure, to building sections with schemas and optimizing performance. Here's where to go next to level up your Shopify theme development skills:
- Shopify Liquid Cheat Sheet — A quick-reference for every Liquid tag, object, and filter. Keep it open while building your theme.
- Shopify Liquid Filters Reference — A complete walkthrough of all filter categories: string, number, array, date, money, URL, and more.
- Shopify Liquid If/Else & Loops Guide — Master conditionals, iteration, and control flow for building dynamic section templates.
- Shopify Liquid Code Examples — 15+ production-ready code snippets organized by use case — product pages, carts, navigation, and more.
- Shopify Liquid for Beginners — If you need to brush up on Liquid fundamentals, start with this beginner-friendly introduction.
Related Guides
Shopify Liquid Cheat Sheet
Quick-reference for tags, objects, and filters.
Shopify Liquid Tutorial
Learn what Liquid is and write your first template code.
Shopify Liquid Filters
Complete reference of string, number, array, and date filters.
If/Else & Loops Guide
Master conditionals and iteration in Liquid templates.
Liquid Code Examples
15+ real-world Shopify Liquid code examples by use case.
We're building an AI Liquid code generator — join the waitlist.
Get early access when we launch. No spam, just one email.