- Understanding Shopify Theme Development: Liquid for Next.js and React Developers
Understanding Shopify Theme Development: Liquid for Next.js and React Developers
A concise mental model and practical guide to Shopify’s Liquid templating language, designed for frontend developers familiar with React or Next.js.

As someone who's spent most of my time building headless storefronts with frameworks like Next.js, I didn’t take Shopify theme development very seriously until recently.
The truth is that theme customization is much more common than headless builds. It is faster to ship, easier to hand off to clients, and more budget-friendly. I wanted to understand how Shopify’s native themes work and what it takes to customize or build one from scratch using Liquid.
This post breaks down the mental model that helped me get up to speed. If you're a frontend or full-stack developer coming from the React or Next.js world and trying to understand Shopify’s Liquid-based theming system, this is for you.
The 80/20 of Shopify Themes: Start Here
Look at the high-level structure. Shopify themes are made of templates, sections, blocks, snippets, and assets.
Once you see how these parts fit together, you know about 80 percent of the system.
Templates
- Templates are top-level pages.
- Each template defines the layout for one page type. This includes product pages, collection pages, cart, homepage, and others.
- Examples:
/templates/product.liquid
,/templates/cart.liquid
,/templates/index.liquid
- Templates usually reference multiple sections.
Sections
- Sections are modular content blocks that you can reuse and customize.
- Some sections are static (hardcoded), others are dynamic (selectable and reorderable in the Theme Editor).
- Each section lives in
/sections/
and contains Liquid code with a{% schema %}
block. - Example sections:
main-product.liquid
,featured-collection.liquid
,testimonial-slider.liquid
Blocks
- Blocks are repeatable components inside a section.
- You can use them for things like testimonials, FAQ entries, or a grid of feature cards.
- Blocks are defined inside the section's schema.
- Merchants can add, remove, or reorder them.
Example:
{% for block in section.blocks %} <div class="testimonial"> <p>{{ block.settings.quote }}</p> <p class="author">{{ block.settings.name }}</p> </div> {% endfor %}
This is like how Payload CMS lets you define repeatable, flexible blocks.
Snippets
- Snippets are reusable chunks of Liquid and HTML.
- They work much like React components.
- Snippets are stored in
/snippets/
and included with{% render 'snippet-name' %}
or{% include 'snippet-name' %}
. - Snippets help keep code modular and clean.
Assets
- These are your static files: CSS, JS, fonts, and images.
- Assets are stored in the
/assets/
folder. - Reference them in Liquid like this:
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}"> <script src="{{ 'app.js' | asset_url }}" defer></script>
You can pre-process CSS (for example, with Tailwind) or JavaScript (for example, with React) and upload the final files here.
How the {% schema %}
Block Works (and Why It Matters)
In Shopify theme development, the {% schema %}
block lets you turn static Liquid files into customizable components. Merchants can then edit these through the Theme Editor without touching code.
In simple terms: Schema lets you define settings and content blocks for the end user (the merchant) to control.
You can enable editing for things like:
- Text fields (heading, subheading, button label)
- Colors, spacing, alignment, and visibility
- Repeating items (testimonials, FAQ entries, image galleries)
The schema is written in JSON and sits at the bottom of a .liquid
file, inside {% schema %}
tags.
Example: A Simple Schema Block for a Section
Say you want a "Text with Image" section. Here’s a simple schema:
{% schema %} { "name": "Text with Image", "settings": [ { "type": "text", "id": "heading", "label": "Heading", "default": "Welcome to our store" }, { "type": "textarea", "id": "text", "label": "Body text" }, { "type": "image_picker", "id": "image", "label": "Image" } ] } {% endschema %}
This will:
- Show fields for heading, text, and an image picker in the Shopify Theme Editor.
- Let the merchant change content visually.
Access these values in your section like this:
<h2>{{ section.settings.heading }}</h2> <p>{{ section.settings.text }}</p> <img src="{{ section.settings.image | image_url }}" alt="">
Blocks in Schema: Repeatable Content
To let a merchant add multiple items, like testimonials, you define blocks in the schema.
{% schema %} { "name": "Testimonials", "settings": [], "blocks": [ { "type": "testimonial", "name": "Testimonial", "settings": [ { "type": "text", "id": "quote", "label": "Quote" }, { "type": "text", "id": "author", "label": "Author Name" } ] } ], "max_blocks": 5 } {% endschema %}
Render blocks like this:
{% for block in section.blocks %} <blockquote>{{ block.settings.quote }}</blockquote> <cite>{{ block.settings.author }}</cite> {% endfor %}
This flexibility is great for both developers and merchants.
Schema in Templates vs Sections
This part is important for anyone who thinks in component logic.
In a section, the {% schema %}
:
- Defines fields and blocks merchants can edit
- Specifies which controls appear in the Theme Editor
In a template, you don’t add {% schema %}
directly.
Modern Shopify themes, especially Online Store 2.0, use JSON for templates. JSON templates define:
- Which sections appear on the page
- Their order
Example: templates/index.json
{
"sections": {
"hero": {
"type": "hero"
},
"rich-text": {
"type": "rich-text"
}
},
"order": ["hero", "rich-text"]
}
This lets merchants control the page structure by adding, removing, or reordering sections. The content for sections is still defined by the section's schema.
Two Ways to Add Content: Native Data and Theme Settings
Once you have the structure, your content comes from one of two places.
Native Shopify Data
Shopify provides data objects, depending on page context.
For example:
- On a product page:
product
- On a collection page:
collection
- On the cart page:
cart
Access them like this:
{{ product.title }} {{ collection.products | size }} {{ cart.total_price | money }}
You do not need to fetch or import this data. Shopify adds it automatically based on routing.
Theme Editor Settings (Schema)
Fields defined in the schema of your sections are editable in the Theme Editor UI.
{{ section.settings.title }} {{ block.settings.label }}
You often combine these with Shopify’s native data for dynamic, editable themes.
Quick Folder Structure Recap
Here's how it fits together in files:
/templates/product.liquid
└── references sections (e.g. main-product, related-products)
/sections/main-product.liquid
└── defines blocks (e.g. features, highlights)
/snippets/
└── reusable logic or markup
/assets/
└── theme.css, app.js, fonts, images
/templates/index.json
└── declares dynamic sections and their order
Bonus: What About Tailwind or React?
You can use Tailwind or React, but you must build them outside Shopify and upload as static assets.
Tailwind CSS
- Set up Tailwind locally.
- Let Tailwind scan
.liquid
files for class usage. - Build your
theme.css
using Tailwind and PurgeCSS. - Upload
theme.css
to/assets/
and link it in your theme layout.
React
- Build with Vite or another bundler.
- Output a bundled JS file (for example,
app.js
). - Upload it to
/assets/
. - Reference the script in your Liquid layout and mount to a DOM node.
I'll cover this workflow in a separate article soon.
Final Thoughts
If you are coming from React or Next.js, Shopify's Liquid system can feel strange at first. Once you understand:
- Templates, sections, and blocks
- Native data vs. Theme Editor settings
- How schema ties everything together
it all becomes clearer.
This mental model helps you build dynamic, flexible, and merchant-friendly themes without overcomplication. As you get more advanced, it's easy to bring in Tailwind, React, or APIs.
Let me know if you want to see more articles about dynamic sections, metafields, performance, or deployment workflows.