
Shopify Theme Development
- 78 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Build and customize Shopify themes using Liquid templating, JSON sections, dynamic blocks, and theme app extensions.
About
Develops Shopify themes with Liquid templating, JSON sections, dynamic blocks, and theme app extensions. A developer uses it to build or customize a merchant's storefront theme.
- Liquid templating with JSON sections and dynamic blocks
- Theme app extensions for added functionality
Shopify Theme Development by the numbers
- 78 all-time installs (skills.sh)
- Ranked #1,122 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill shopify-theme-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 78 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Build and customize Shopify themes using Liquid templating, JSON sections, dynamic blocks, and theme app extensions.
Files
Shopify Theme Development
Overview
Build and customize Shopify themes using Liquid templating, JSON templates, sections and blocks for merchant-customizable layouts, and theme app extensions for app integrations. This skill covers the Shopify theme architecture (Online Store 2.0), the Shopify CLI development workflow, performance optimization with lazy loading and critical CSS, and patterns for building flexible sections that merchants can configure through the theme editor.
When to Use This Skill
- When building a new Shopify theme from scratch or forking Dawn
- When creating custom sections and blocks for the theme editor
- When implementing product pages, collection grids, or cart functionality in Liquid
- When optimizing a Shopify theme for Core Web Vitals and speed
- When building theme app extensions to inject app content into themes
Core Instructions
1. Set up the development environment with Shopify CLI
# Install Shopify CLI
npm install -g @shopify/cli @shopify/theme
# Initialize a new theme (or clone Dawn)
shopify theme init my-theme
# Start development server with hot reload
shopify theme dev --store=your-store.myshopify.comTheme directory structure (Online Store 2.0):
my-theme/
├── assets/ # CSS, JS, images
├── config/ # settings_schema.json, settings_data.json
├── layout/ # theme.liquid (main layout)
├── locales/ # Translation files
├── sections/ # Sections (reusable, merchant-configurable)
├── snippets/ # Partials (reusable Liquid fragments)
└── templates/ # JSON templates referencing sections
├── product.json
├── collection.json
└── index.json2. Create a JSON template with sections
// templates/product.json
{
"sections": {
"main": {
"type": "main-product",
"settings": {}
},
"recommendations": {
"type": "product-recommendations",
"settings": {
"heading": "You may also like",
"products_to_show": 4
}
},
"reviews": {
"type": "product-reviews",
"settings": {}
}
},
"order": ["main", "recommendations", "reviews"]
}3. Build a customizable product section with blocks
{% comment %}
sections/main-product.liquid
{% endcomment %}
<section class="product-section" data-section-id="{{ section.id }}">
<div class="product-grid">
<div class="product-media">
{% for media in product.media %}
{% case media.media_type %}
{% when 'image' %}
<div class="product-media-item {% if forloop.first %}active{% endif %}">
{{ media | image_url: width: 800 | image_tag:
loading: 'lazy',
widths: '200,400,600,800,1000',
sizes: '(min-width: 768px) 50vw, 100vw',
class: 'product-image'
}}
</div>
{% when 'video' %}
<div class="product-media-item">
{{ media | video_tag: autoplay: false, controls: true }}
</div>
{% endcase %}
{% endfor %}
</div>
<div class="product-info">
{% for block in section.blocks %}
{% case block.type %}
{% when 'title' %}
<h1 class="product-title" {{ block.shopify_attributes }}>
{{ product.title }}
</h1>
{% when 'price' %}
<div class="product-price" {{ block.shopify_attributes }}>
{% if product.compare_at_price > product.price %}
<s class="price-compare">{{ product.compare_at_price | money }}</s>
{% endif %}
<span class="price-current">{{ product.price | money }}</span>
{% if product.compare_at_price > product.price %}
<span class="price-badge">Sale</span>
{% endif %}
</div>
{% when 'variant_picker' %}
<div class="variant-picker" {{ block.shopify_attributes }}>
{% for option in product.options_with_values %}
<fieldset class="option-group">
<legend>{{ option.name }}</legend>
{% for value in option.values %}
<label class="option-label">
<input
type="radio"
name="{{ option.name }}"
value="{{ value }}"
{% if option.selected_value == value %}checked{% endif %}
>
<span>{{ value }}</span>
</label>
{% endfor %}
</fieldset>
{% endfor %}
</div>
{% when 'buy_buttons' %}
<div class="buy-buttons" {{ block.shopify_attributes }}>
{% form 'product', product %}
<input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
<div class="quantity-selector">
<label for="quantity">Quantity</label>
<input type="number" id="quantity" name="quantity" value="1" min="1">
</div>
<button
type="submit"
class="btn btn-primary add-to-cart"
{% unless product.selected_or_first_available_variant.available %}disabled{% endunless %}
>
{% if product.selected_or_first_available_variant.available %}
Add to cart — {{ product.selected_or_first_available_variant.price | money }}
{% else %}
Sold out
{% endif %}
</button>
{% endform %}
</div>
{% when 'description' %}
<div class="product-description" {{ block.shopify_attributes }}>
{{ product.description }}
</div>
{% when 'custom_text' %}
<div class="custom-text" {{ block.shopify_attributes }}>
{{ block.settings.text }}
</div>
{% endcase %}
{% endfor %}
</div>
</div>
</section>
{% schema %}
{
"name": "Product Page",
"tag": "section",
"class": "section-product",
"blocks": [
{
"type": "title",
"name": "Title",
"limit": 1
},
{
"type": "price",
"name": "Price",
"limit": 1
},
{
"type": "variant_picker",
"name": "Variant Picker",
"limit": 1
},
{
"type": "buy_buttons",
"name": "Buy Buttons",
"limit": 1
},
{
"type": "description",
"name": "Description",
"limit": 1
},
{
"type": "custom_text",
"name": "Custom Text",
"settings": [
{
"type": "richtext",
"id": "text",
"label": "Text"
}
]
}
],
"presets": [
{
"name": "Product Page",
"blocks": [
{ "type": "title" },
{ "type": "price" },
{ "type": "variant_picker" },
{ "type": "buy_buttons" },
{ "type": "description" }
]
}
]
}
{% endschema %}4. Implement AJAX cart with the Cart API
// assets/cart.js
class CartManager {
async addItem(variantId, quantity = 1) {
const response = await fetch('/cart/add.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: variantId, quantity }],
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.description || 'Could not add to cart');
}
const data = await response.json();
this.updateCartUI();
return data;
}
async updateQuantity(lineKey, quantity) {
const response = await fetch('/cart/change.js', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: lineKey, quantity }),
});
const cart = await response.json();
this.updateCartUI(cart);
return cart;
}
async getCart() {
const response = await fetch('/cart.js');
return response.json();
}
async updateCartUI(cart) {
cart = cart || await this.getCart();
// Update cart count badge
const badge = document.querySelector('[data-cart-count]');
if (badge) badge.textContent = cart.item_count;
// Update cart drawer if open
const drawer = document.querySelector('cart-drawer');
if (drawer) drawer.render(cart);
}
}
window.cart = new CartManager();5. Optimize for performance
{% comment %}
layout/theme.liquid — Critical performance optimizations
{% endcomment %}
<!doctype html>
<html lang="{{ request.locale.iso_code }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Preconnect to Shopify CDN -->
<link rel="preconnect" href="https://cdn.shopify.com" crossorigin>
<link rel="preconnect" href="https://fonts.shopifycdn.com" crossorigin>
<!-- Preload critical assets -->
{% if template.name == 'product' %}
{% assign hero_image = product.featured_image %}
{% if hero_image %}
<link
rel="preload"
as="image"
href="{{ hero_image | image_url: width: 800 }}"
imagesrcset="{{ hero_image | image_url: width: 400 }} 400w,
{{ hero_image | image_url: width: 800 }} 800w"
imagesizes="(min-width: 768px) 50vw, 100vw"
>
{% endif %}
{% endif %}
<!-- Inline critical CSS -->
<style>
{{ 'critical.css' | asset_url | stylesheet_tag | split: '<link' | first }}
</style>
<!-- Defer non-critical CSS -->
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}" media="print" onload="this.media='all'">
<noscript><link rel="stylesheet" href="{{ 'theme.css' | asset_url }}"></noscript>
{{ content_for_header }}
</head>
<body>
{{ content_for_layout }}
<!-- Defer JavaScript -->
<script src="{{ 'theme.js' | asset_url }}" defer></script>
</body>
</html>6. Create a theme app extension
# Generate a theme app extension block
shopify app generate extension --type theme_app_extension --name my-app-block {% comment %}
extensions/my-app-block/blocks/product-badge.liquid
{% endcomment %}
<div class="app-product-badge" {{ block.shopify_attributes }}>
{% if block.settings.badge_text != blank %}
<span
class="badge"
style="background-color: {{ block.settings.badge_color }};
color: {{ block.settings.text_color }};"
>
{{ block.settings.badge_text }}
</span>
{% endif %}
</div>
{% schema %}
{
"name": "Product Badge",
"target": "section",
"settings": [
{
"type": "text",
"id": "badge_text",
"label": "Badge text",
"default": "New"
},
{
"type": "color",
"id": "badge_color",
"label": "Badge color",
"default": "#FF0000"
},
{
"type": "color",
"id": "text_color",
"label": "Text color",
"default": "#FFFFFF"
}
]
}
{% endschema %}Examples
Collection grid with lazy-loaded images
{% comment %} sections/collection-grid.liquid {% endcomment %}
<section class="collection-grid">
<h1>{{ collection.title }}</h1>
<div class="product-grid" role="list">
{% paginate collection.products by 24 %}
{% for product in collection.products %}
<div class="product-card" role="listitem">
<a href="{{ product.url }}">
{% if product.featured_image %}
{{ product.featured_image | image_url: width: 400 | image_tag:
loading: 'lazy',
widths: '200,300,400',
sizes: '(min-width: 1024px) 25vw, (min-width: 768px) 33vw, 50vw',
class: 'product-card-image'
}}
{% endif %}
<h2 class="product-card-title">{{ product.title }}</h2>
<p class="product-card-price">{{ product.price | money }}</p>
</a>
</div>
{% endfor %}
{% if paginate.pages > 1 %}
<nav class="pagination" aria-label="Pagination">
{{ paginate | default_pagination: next: 'Next', previous: 'Previous' }}
</nav>
{% endif %}
{% endpaginate %}
</div>
</section>Variant change with JavaScript
// assets/variant-selector.js
class VariantSelector extends HTMLElement {
connectedCallback() {
this.addEventListener('change', this.onVariantChange.bind(this));
this.productData = JSON.parse(
this.querySelector('[type="application/json"]').textContent
);
}
onVariantChange() {
const selectedOptions = [...this.querySelectorAll('input:checked')].map(
input => input.value
);
const variant = this.productData.variants.find(v =>
v.options.every((opt, i) => opt === selectedOptions[i])
);
if (!variant) return;
// Update URL without reload
const url = new URL(window.location);
url.searchParams.set('variant', variant.id);
window.history.replaceState({}, '', url);
// Update price display
const priceEl = document.querySelector('.price-current');
if (priceEl) {
priceEl.textContent = this.formatMoney(variant.price);
}
// Update add-to-cart button
const addToCart = document.querySelector('.add-to-cart');
const idInput = document.querySelector('input[name="id"]');
if (addToCart && idInput) {
idInput.value = variant.id;
addToCart.disabled = !variant.available;
addToCart.textContent = variant.available ? 'Add to cart' : 'Sold out';
}
}
formatMoney(cents) {
return '$' + (cents / 100).toFixed(2);
}
}
customElements.define('variant-selector', VariantSelector);Best Practices
- Use Online Store 2.0 JSON templates — they enable merchants to add, remove, and reorder sections without editing code
- Leverage the `image_url` and `image_tag` filters — they generate responsive
srcsetattributes automatically from Shopify's CDN - Always include `{{ block.shopify_attributes }}` — this data attribute is required for the theme editor to identify and select blocks
- Defer all JavaScript — use
deferortype="module"on script tags; avoid render-blocking JS - Use Shopify's native Liquid filters —
| money,| image_url,| asset_urlare optimized and handle edge cases; don't reinvent them - Provide section presets — presets define the default state when a merchant adds a section; without them, the section won't appear in "Add section"
- Keep sections under 50KB of Liquid — large sections slow down the Liquid renderer; extract reusable code into snippets
- Test on Shopify's staging theme — always preview changes on an unpublished theme before deploying to the live theme
Common Pitfalls
| Problem | Solution |
|---|---|
| Section not appearing in "Add section" menu | Ensure the section has a presets array in its {% schema %}; sections without presets are only available in JSON templates |
| Theme editor shows "Error rendering section" | Check for Liquid syntax errors; use shopify theme check to lint your Liquid code |
| Images not loading on Shopify CDN | Use image_url filter with explicit width parameter; the old img_url filter is deprecated |
| Cart count badge not updating after AJAX add | Fetch /cart.js after every cart mutation and update the badge; don't rely on the response from add.js alone |
| Slow Largest Contentful Paint (LCP) | Preload the hero/product image in <head> using <link rel="preload" as="image">; inline critical CSS |
| Metafields not accessible in Liquid | Ensure metafield definitions are created in Shopify admin; access via product.metafields.namespace.key |
Related Skills
- @product-page-design
- @ecommerce-seo
- @ecommerce-caching
- @storefront-performance
- @shopify-app-development
{
"context": "Tests whether the agent builds a Shopify collection grid section using correct Online Store 2.0 JSON templates, native Liquid pagination tags, responsive lazy-loaded images via image_url/image_tag, native money filter, deferred JavaScript, and proper schema presets.",
"type": "weighted_checklist",
"checklist": [
{
"name": "JSON template for collection",
"max_score": 12,
"description": "A `templates/collection.json` file is produced that references the collection section by key with an `order` array, rather than a Liquid `.liquid` template"
},
{
"name": "paginate tag used",
"max_score": 10,
"description": "Product listing uses `{% paginate collection.products by N %}...{% endpaginate %}` Liquid tags rather than manual slicing"
},
{
"name": "default_pagination filter",
"max_score": 10,
"description": "Pagination navigation uses `{{ paginate | default_pagination }}` filter rather than manually constructing pagination links"
},
{
"name": "image_url not img_url",
"max_score": 10,
"description": "Product card images use `image_url` filter with an explicit `width` parameter — the deprecated `img_url` filter is NOT used anywhere"
},
{
"name": "image_tag lazy loading",
"max_score": 8,
"description": "Product card images use `image_tag` with `loading: 'lazy'`"
},
{
"name": "image_tag widths and sizes",
"max_score": 8,
"description": "Product card `image_tag` calls include both `widths` and `sizes` parameters for responsive images"
},
{
"name": "money filter for price",
"max_score": 8,
"description": "Product card prices use the `| money` Liquid filter, not manual cent division or string formatting"
},
{
"name": "Schema presets present",
"max_score": 10,
"description": "The section's `{% schema %}` includes a non-empty `presets` array"
},
{
"name": "block.shopify_attributes on blocks",
"max_score": 8,
"description": "If the section defines any blocks, every block wrapper element includes `{{ block.shopify_attributes }}`"
},
{
"name": "JS deferred or module",
"max_score": 8,
"description": "Any `<script>` tags in the section or included assets use `defer` attribute or `type=\"module\"` — no synchronous render-blocking script tags"
},
{
"name": "asset_url filter for assets",
"max_score": 8,
"description": "CSS and JS files are referenced using `| asset_url` filter rather than hardcoded paths"
}
]
}
Build a Shopify Collection Page Section
Problem/Feature Description
A home goods retailer is launching a new Shopify store and needs a collection browsing page. Their catalog has hundreds of products across several collections, and shoppers need to be able to browse product thumbnails quickly without the page feeling slow or overwhelming. The team's previous theme used an older Shopify architecture, and the new theme must support the modern theme editor workflow so the marketing team can adjust the layout themselves.
The collection page should show product cards in a responsive grid, with the product name and price visible under each card. Because many collections have more than 24 products, the page needs to support pagination. Page load speed is a priority: product card images should load efficiently on all screen sizes and should not block the initial render.
Additionally, the section needs to be configurable from the theme editor, so merchants can tune the number of products per page and optionally enable a sort/filter bar.
Output Specification
Produce the following theme files:
sections/collection-grid.liquid— the section file with schematemplates/collection.json— the JSON template that wires up the section- Any supporting JS should be placed in
assets/and referenced appropriately
The section should render a responsive product grid with card images, titles, and prices. Pagination should work for large collections. Include at least one configurable section setting (e.g. products per page) in the schema.
{
"context": "Tests whether the agent correctly implements a performance-optimized Shopify theme layout with critical CSS inlining, deferred non-critical CSS, hero image preloading, CDN preconnect hints, deferred JS, and a correct AJAX cart implementation that fetches /cart.js after mutations and updates the badge.",
"type": "weighted_checklist",
"checklist": [
{
"name": "preconnect to Shopify CDN",
"max_score": 8,
"description": "The layout/theme.liquid `<head>` includes `<link rel=\"preconnect\" href=\"https://cdn.shopify.com\" crossorigin>` and `<link rel=\"preconnect\" href=\"https://fonts.shopifycdn.com\" crossorigin>`"
},
{
"name": "Hero image preload tag",
"max_score": 10,
"description": "A `<link rel=\"preload\" as=\"image\">` tag is conditionally rendered in `<head>` for the product featured image when on a product template (checks `template.name == 'product'`)"
},
{
"name": "Critical CSS inlined",
"max_score": 10,
"description": "Critical CSS content is inlined inside a `<style>` tag in `<head>` rather than loaded via an external `<link>` tag"
},
{
"name": "Non-critical CSS deferred",
"max_score": 10,
"description": "Non-critical CSS (e.g. theme.css) uses the `media=\"print\" onload=\"this.media='all'\"` pattern to defer loading, with a `<noscript>` fallback"
},
{
"name": "JS uses defer attribute",
"max_score": 8,
"description": "Main theme JavaScript is loaded with `defer` attribute (or `type=\"module\"`) on the `<script>` tag — no render-blocking synchronous script tags"
},
{
"name": "Cart API add endpoint",
"max_score": 8,
"description": "AJAX add-to-cart uses POST to `/cart/add.js` with JSON body containing `items` array"
},
{
"name": "Cart API change endpoint",
"max_score": 8,
"description": "Cart quantity update uses POST to `/cart/change.js` with `id` and `quantity` fields"
},
{
"name": "Fetch /cart.js after mutation",
"max_score": 12,
"description": "After any cart mutation (add or update), `/cart.js` is fetched separately to retrieve the current cart state for UI update — NOT relying solely on the response from `/cart/add.js`"
},
{
"name": "Cart count badge updated",
"max_score": 10,
"description": "The cart item count badge (selected by a data attribute like `[data-cart-count]`) is updated with `cart.item_count` from the `/cart.js` response"
},
{
"name": "asset_url for stylesheets",
"max_score": 8,
"description": "CSS files are referenced using `| asset_url` Liquid filter rather than hardcoded paths or CDN URLs"
},
{
"name": "content_for_header tag",
"max_score": 8,
"description": "The layout includes `{{ content_for_header }}` inside `<head>` and `{{ content_for_layout }}` in the `<body>`"
}
]
}
Optimize Shopify Theme Layout and Implement AJAX Cart
Problem/Feature Description
A fashion e-commerce brand has been struggling with poor Core Web Vitals scores on their Shopify store. Their Google Search Console shows consistently low scores for Largest Contentful Paint and Total Blocking Time, which is hurting both SEO rankings and conversion rates. A performance audit identified that the main theme layout loads all CSS and JavaScript synchronously, there are no resource hints for the Shopify CDN, and the hero product image is not prioritized.
Additionally, their "Add to Cart" button does a full page reload, which creates a jarring experience. The team wants a seamless AJAX cart where clicking "Add to Cart" updates a cart count badge in the header without reloading the page. A known pain point from their previous AJAX implementation was that the cart badge count sometimes got out of sync after adding items — the old code read the count directly from the add response rather than verifying the true cart state.
Your task is to produce an optimized layout/theme.liquid file and a assets/cart.js file that address both problems.
Output Specification
Produce the following files:
layout/theme.liquid— a complete theme layout with performance optimizations in the<head>and body, including a cart count badge element in the header areaassets/cart.js— a JavaScript cart manager class that handles add-to-cart, quantity updates, cart state retrieval, and header badge updates
The layout should handle both product and non-product pages appropriately. The cart implementation should expose a usable API (e.g. window.cart) for other scripts to call.
{
"context": "Tests whether the agent correctly builds a merchant-customizable Shopify product section with proper block attributes, schema presets, native Liquid filters, and variant-aware UI. Covers core Online Store 2.0 section patterns required for theme editor compatibility.",
"type": "weighted_checklist",
"checklist": [
{
"name": "block.shopify_attributes present",
"max_score": 12,
"description": "Every block's wrapper element includes `{{ block.shopify_attributes }}` as an attribute"
},
{
"name": "Schema has presets array",
"max_score": 12,
"description": "The section's {% schema %} JSON includes a non-empty `presets` array so the section appears in the theme editor 'Add section' menu"
},
{
"name": "image_url filter used",
"max_score": 10,
"description": "Product images are rendered using the `image_url` filter (with an explicit `width` parameter), not the deprecated `img_url` filter"
},
{
"name": "image_tag with lazy loading",
"max_score": 8,
"description": "The `image_tag` filter is used with `loading: 'lazy'` for product media images"
},
{
"name": "image_tag widths and sizes",
"max_score": 8,
"description": "The `image_tag` filter includes both `widths` and `sizes` parameters to generate a responsive srcset"
},
{
"name": "money filter for prices",
"max_score": 8,
"description": "All price output uses the `| money` Liquid filter rather than manual formatting"
},
{
"name": "product form tag used",
"max_score": 10,
"description": "The add-to-cart form uses `{% form 'product', product %}` rather than a plain HTML `<form>` tag"
},
{
"name": "selected_or_first_available_variant",
"max_score": 10,
"description": "The hidden variant ID input and availability logic reference `product.selected_or_first_available_variant` (not `product.variants.first` or a hardcoded variant)"
},
{
"name": "Blocks iterated via section.blocks",
"max_score": 8,
"description": "The section renders content by looping over `section.blocks` with `{% for block in section.blocks %}` and using `block.type`"
},
{
"name": "JSON template uses section reference",
"max_score": 14,
"description": "A JSON template file (e.g. product.json) is produced that references the section by key, with an `order` array — not an inline Liquid template"
}
]
}
Build a Customizable Product Section for a Shopify Theme
Problem/Feature Description
A direct-to-consumer apparel brand is migrating their Shopify store to a new custom theme. The merchandising team needs a product detail page that their non-technical staff can configure through the Shopify theme editor — rearranging the page content, toggling individual elements, and tweaking copy — without touching code. Currently the product page is a monolithic Liquid file that hardcodes every element, so merchants can't customize anything without a developer.
Your task is to build the product section and its accompanying template for the new theme. The section should support the most common product page content blocks (title, price with sale display, variant picker via radio buttons, add-to-cart button that reflects availability, and product description). It should be structured so that the theme editor can identify each block, and the section should be discoverable in the editor's "Add section" panel.
Output Specification
Produce the following files representing the theme structure:
sections/main-product.liquid— the Liquid section with schematemplates/product.json— the JSON template that uses the section- Any snippets you extract for reusability should go under
snippets/
The section must work with the standard Shopify product object and support multiple product media types (images and video at minimum). Prices should handle sale scenarios (compare-at price vs current price).
{
"name": "finsi/shopify-theme-development",
"version": "0.1.0",
"summary": "Liquid templating, theme architecture, sections, and theme app extensions",
"skills": {
"shopify-theme-development": {
"path": "SKILL.md"
}
}
}