Theme architecture is the difference between a Shopify store that is a pleasure to maintain and one that becomes a liability. A well-structured theme scales gracefully as your catalogue grows, your content requirements evolve, and your team expands. A poorly structured theme accumulates technical debt with every change, eventually reaching a point where simple updates take hours and introducing new features risks breaking existing functionality.
This guide covers the architectural patterns that underpin maintainable, performant Shopify themes. These are the patterns we use across every custom Shopify theme we build as part of our Shopify development services.
Theme file structure overview
Every Shopify theme follows a prescribed directory structure. Understanding what each directory does and how the files within it interact is fundamental to working effectively with Shopify themes.
my-theme/
├── assets/ # CSS, JS, images, fonts
│ ├── base.css
│ ├── component-card.css
│ ├── section-hero.css
│ ├── global.js
│ └── product-form.js
├── config/ # Theme settings
│ ├── settings_schema.json # Settings UI definition
│ └── settings_data.json # Current settings values
├── layout/ # Wrapper templates
│ ├── theme.liquid # Main layout
│ └── password.liquid # Password page layout
├── locales/ # Translation files
│ ├── en.default.json
│ └── en.default.schema.json
├── sections/ # Modular content sections
│ ├── header.liquid
│ ├── footer.liquid
│ ├── hero-banner.liquid
│ ├── featured-collection.liquid
│ └── rich-text.liquid
├── snippets/ # Reusable code fragments
│ ├── card-product.liquid
│ ├── icon.liquid
│ ├── price.liquid
│ └── responsive-image.liquid
└── templates/ # Page templates (JSON)
├── index.json
├── product.json
├── collection.json
├── page.json
├── blog.json
├── article.json
├── cart.json
└── 404.json
The rendering hierarchy
When a customer visits a page, Shopify resolves the URL to a template, which references sections, which may include snippets. The layout wraps everything. Understanding this hierarchy is critical for debugging rendering issues:
- Layout (theme.liquid) — renders the HTML shell, head, navigation, footer, and outputs
{{ content_for_layout }}. - Template (e.g., product.json) — defines which sections to render and their order.
- Sections (e.g., product-hero.liquid) — self-contained content modules with their own schema, settings, and blocks.
- Snippets (e.g., card-product.liquid) — reusable fragments rendered within sections via
{%- render 'snippet-name' -%}.
JSON templates and sections everywhere
Shopify’s Online Store 2.0 introduced JSON templates, which replaced the older Liquid templates as the primary template format. This was a fundamental shift in how themes are structured.
A JSON template does not contain any rendering logic. Instead, it defines which sections appear on the page and their configuration:
// templates/product.json
{
"sections": {
"main": {
"type": "main-product",
"settings": {
"show_vendor": true,
"show_sku": false,
"media_size": "large"
},
"blocks": {
"title": { "type": "title", "order": 1 },
"price": { "type": "price", "order": 2 },
"variant_picker": { "type": "variant_picker", "order": 3 },
"quantity": { "type": "quantity_selector", "order": 4 },
"buy_buttons": { "type": "buy_buttons", "order": 5 },
"description": { "type": "description", "order": 6 }
}
},
"recommendations": {
"type": "product-recommendations",
"settings": {
"heading": "You may also like",
"products_to_show": 4
}
}
},
"order": ["main", "recommendations"]
}
The “sections everywhere” approach means merchants can customise every page type through the theme editor, adding, removing, and reordering sections without touching code. This is a significant advantage for ongoing content management.
Section and block architecture
Sections are the building blocks of a Shopify theme. A well-designed section is self-contained, configurable through the theme editor, and reusable across multiple templates.
Section structure
<!-- sections/featured-collection.liquid -->
{%- liquid
assign collection = section.settings.collection
assign products_to_show = section.settings.products_to_show
assign columns = section.settings.columns
-%}
<section id="section-{{ section.id }}" class="featured-collection section-padding">
<div class="container">
{%- if section.settings.heading != blank -%}
<h2 class="featured-collection__heading h2">{{ section.settings.heading | escape }}</h2>
{%- endif -%}
<div class="grid grid--{{ columns }}-col">
{%- for product in collection.products limit: products_to_show -%}
{%- render 'card-product', product: product, show_vendor: section.settings.show_vendor -%}
{%- endfor -%}
</div>
{%- if section.settings.show_view_all -%}
<div class="featured-collection__footer">
<a href="{{ collection.url }}" class="btn btn--secondary">View all {{ collection.title | escape }}</a>
</div>
{%- endif -%}
</div>
</section>
{% schema %}
{
"name": "Featured collection",
"tag": "section",
"settings": [
{ "type": "text", "id": "heading", "label": "Heading", "default": "Featured collection" },
{ "type": "collection", "id": "collection", "label": "Collection" },
{ "type": "range", "id": "products_to_show", "min": 2, "max": 12, "step": 1, "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" },
{ "type": "checkbox", "id": "show_vendor", "label": "Show vendor", "default": false },
{ "type": "checkbox", "id": "show_view_all", "label": "Show view all button", "default": true }
],
"presets": [{ "name": "Featured collection" }]
}
{% endschema %}
Block patterns
Blocks allow merchants to add, remove, and reorder content within a section. They are particularly powerful for product pages, where different products may need different information layouts:
{%- for block in section.blocks -%}
{%- case block.type -%}
{%- when 'title' -%}
<h1 class="product__title" {{ block.shopify_attributes }}>{{ product.title | escape }}</h1>
{%- when 'price' -%}
<div class="product__price" {{ block.shopify_attributes }}>
{%- render 'price', product: product -%}
</div>
{%- when 'collapsible_tab' -%}
<details class="product__accordion" {{ block.shopify_attributes }}>
<summary>{{ block.settings.heading | escape }}</summary>
<div class="product__accordion-content">{{ block.settings.content }}</div>
</details>
{%- endcase -%}
{%- endfor -%}
Liquid rendering order
Understanding when and how Liquid renders is essential for debugging and performance. Liquid is a server-side template language — all Liquid code executes on Shopify’s servers before the HTML is sent to the browser.
Variable scoping
Liquid variable scoping in Shopify has specific rules that catch developers off guard:
<!-- Variables assigned in a section are NOT available in snippets called with render -->
{%- assign my_var = 'hello' -%}
{%- render 'my-snippet' -%}
<!-- my_var is NOT accessible inside my-snippet.liquid -->
<!-- Pass variables explicitly -->
{%- render 'my-snippet', my_var: my_var -%}
<!-- Always use render instead of include. Explicit variable passing makes dependencies clear. -->
Asset pipeline and performance
Shopify’s asset pipeline serves files from the assets/ directory via Shopify’s CDN. Understanding how assets are loaded and cached is critical for theme performance.
CSS strategy
<!-- Load critical CSS inline in the head -->
<style>{{ 'critical.css' | asset_url | inline_asset_content }}</style>
<!-- Load non-critical CSS asynchronously -->
<link rel="stylesheet" href="{{ 'base.css' | asset_url }}" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="{{ 'base.css' | asset_url }}"></noscript>
<!-- Section-specific CSS loaded only when needed -->
{%- if section.settings.enable_video -%}
{{ 'component-video.css' | asset_url | stylesheet_tag }}
{%- endif -%}
JavaScript strategy
<!-- Defer non-critical JavaScript -->
<script src="{{ 'global.js' | asset_url }}" defer></script>
<!-- Use web components for section interactivity -->
<script type="module">
class ProductForm extends HTMLElement {
connectedCallback() {
this.form = this.querySelector('form');
this.form.addEventListener('submit', this.handleSubmit.bind(this));
}
async handleSubmit(e) {
e.preventDefault();
const formData = new FormData(this.form);
await fetch('/cart/add.js', { method: 'POST', body: formData });
}
}
customElements.define('product-form', ProductForm);
</script>
Snippet patterns for reusability
Snippets are the primary mechanism for code reuse in Shopify themes. Well-designed snippets reduce duplication, improve consistency, and make maintenance significantly easier.
<!-- snippets/card-product.liquid -->
{%- comment -%}
Renders a product card.
Accepts:
- product: {Object} Product Liquid object (required)
- show_vendor: {Boolean} Show vendor name (optional, default: false)
- lazy_load: {Boolean} Lazy load images (optional, default: true)
Usage: {%- render 'card-product', product: product, show_vendor: true -%}
{%- endcomment -%}
{%- liquid
assign show_vendor = show_vendor | default: false
assign lazy_load = lazy_load | default: true
-%}
<div class="card-product">
<a href="{{ product.url }}" class="card-product__link">
<div class="card-product__media">
{%- if product.featured_image -%}
{%- render 'responsive-image', image: product.featured_image, sizes: '(min-width: 1200px) 25vw, (min-width: 768px) 33vw, 50vw', lazy: lazy_load -%}
{%- else -%}
<div class="card-product__placeholder">{{ 'product-1' | placeholder_svg_tag: 'placeholder-svg' }}</div>
{%- endif -%}
</div>
<div class="card-product__info">
{%- if show_vendor and product.vendor != blank -%}
<span class="card-product__vendor">{{ product.vendor | escape }}</span>
{%- endif -%}
<h3 class="card-product__title">{{ product.title | escape }}</h3>
{%- render 'price', product: product -%}
</div>
</a>
</div>
Schema design for the theme editor
The schema in each section file defines what merchants see in the theme editor. Good schema design makes the theme intuitive for non-technical users:
- Use clear, descriptive labels that non-developers understand.
- Provide sensible defaults for every setting.
- Group related settings using
headerandparagraphinfo types. - Use
rangeinputs instead of free text for numeric values. - Limit block types to those that genuinely make sense within the section.
Version control strategies
Every Shopify theme should be version controlled with Git. The challenge is managing the relationship between the Git repository and the live theme:
# .gitignore for Shopify themes
config/settings_data.json # Contains merchant customisations
*.json~ # Editor backup files
.shopify/ # CLI state
The key principle: developers control the schema (settings_schema.json), merchants control the data (settings_data.json). For automated deployments, see our guide on CI/CD for Shopify themes.
Testing and quality assurance
Testing checklist
- Test every section with minimum content (empty fields, no images).
- Test every section with maximum content (long titles, many blocks).
- Test product pages with 1 variant and with 100+ variants.
- Test collection pages with 1 product and with 500+ products.
- Test across browsers (Chrome, Firefox, Safari, Edge).
- Test across devices (mobile, tablet, desktop).
- Test with screen readers (VoiceOver, NVDA).
- Test keyboard navigation through all interactive elements.
Scaling patterns for large catalogues
Themes that work well with 50 products can struggle with 5,000. Scaling requires attention to pagination, filtering performance, and Liquid rendering efficiency.
<!-- Use paginate for large collections -->
{%- paginate collection.products by 24 -%}
<div class="collection-grid">
{%- for product in collection.products -%}
{%- render 'card-product', product: product, lazy_load: true -%}
{%- endfor -%}
</div>
{%- if paginate.pages > 1 -%}
{%- render 'pagination', paginate: paginate -%}
{%- endif -%}
{%- endpaginate -%}
For stores with large catalogues, consider implementing AJAX-based filtering and infinite scroll to avoid full page reloads. This is where the intersection of theme architecture and metafields becomes particularly important — structured product data enables sophisticated filtering without performance degradation.
Theme architecture is a long-term investment. The decisions you make when setting up the file structure, section patterns, and coding conventions will compound over the lifetime of the theme. If you need a theme built with proper architecture from the start, get in touch — it is a core part of what we do in our Shopify development work.