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

text
<!-- 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.

bash
# 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.com

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

bash
# 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 open

Building 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 }}.

liquid
<!-- 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.

json
// 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.

liquid
<!-- 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.

json
// 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.

liquid
<!-- 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.

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.

json
// 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.

liquid
<!-- 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.

liquid
<!-- 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.

liquid
<!-- 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:

Related Guides

We're building an AI Liquid code generator — join the waitlist.

Get early access when we launch. No spam, just one email.