This skill should be used when working with Shopify themes, Liquid templating, theme blocks, sections, dynamic sources, or any Shopify theme development tasks. Provides comprehensive guidance on building production-quality Shopify themes with focus on Horizon theme patterns, blocks architecture, schema design, and Liquid best practices.
How this skill is triggered — by the user, by Claude, or both
Slash command
/shopify-liquid-designer:shopify-liquid-geniusThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Complete reference for building production-quality Shopify themes using Liquid, with special focus on Horizon theme patterns, theme blocks architecture, dynamic sources, and modern theme development.
Complete reference for building production-quality Shopify themes using Liquid, with special focus on Horizon theme patterns, theme blocks architecture, dynamic sources, and modern theme development.
Use this skill when:
/assets/ CSS, JS, images, fonts
/blocks/ Theme blocks (reusable across sections)
/config/ Theme settings, color schemes
/layout/ theme.liquid, password.liquid
/locales/ Translation files
/sections/ Section files (.liquid)
/snippets/ Reusable components (.liquid)
/templates/ Page templates (.json)
Know which files belong to apps vs native theme:
| Prefix | Type | Can Modify? |
|---|---|---|
hs-custom-* | Custom sections | ✅ YES |
ecom-* | EComposer App | ⚠️ CAREFUL |
ss-* | SectionStore App | ⚠️ CAREFUL |
foxify-* | Foxify App | ⚠️ CAREFUL |
Warning: Third-party apps may leave traces throughout theme files. Don't delete app comments or markers without understanding dependencies.
1. Theme Blocks (Most Flexible)
/blocks/ folder2. Section Blocks (Section-Specific)
3. App Blocks (Third-Party)
@app| Feature | Theme Blocks | Snippets |
|---|---|---|
| Location | /blocks/ | /snippets/ |
| Has Schema | ✅ YES | ❌ NO |
| Theme Editor | ✅ Merchants can configure | ❌ Developer only |
| Nestable | ✅ YES (8 levels) | ❌ NO |
| Pass Variables | ❌ NO | ✅ YES |
| Dynamic Sources | ✅ YES | ❌ NO |
Best Practice - Combined Pattern:
<!-- /blocks/product-card.liquid -->
<div {{ block.shopify_attributes }}>
{%- render 'product-card-markup',
product: block.settings.product,
show_vendor: block.settings.show_vendor
-%}
</div>
closest PatternCRITICAL: Theme blocks don't know their context ahead of time. Use closest.<type> to access the nearest resource of that type.
<!-- /blocks/price.liquid -->
<span class="price">
{{ closest.product.price | money }}
</span>
Resolution order for closest.product:
Available types: closest.product, closest.collection, closest.article, closest.page, closest.blog
Via content_for tag:
{% content_for "blocks", product: featured_product %}
Via static block rendering:
{%- for product in collection.products -%}
{% content_for "block",
type: "product-card",
id: "card-{{ product.id }}",
product: product
%}
{%- endfor -%}
Compatible setting types for dynamic sources:
| Setting Type | Can Use Dynamic Source? | Example Source |
|---|---|---|
text | ✅ YES | product.title |
image_picker | ✅ YES | product.featured_image |
url | ✅ YES | product.url |
richtext | ✅ YES | product.description |
color | ❌ NO | - |
checkbox | ❌ NO | - |
Example:
{
"type": "text",
"id": "heading",
"label": "Heading",
"default": "{{ product.title }}"
}
Limitations:
{% schema %}
{
"name": "Section Name",
"tag": "section",
"class": "custom-class",
"settings": [
/* Section-level settings */
],
"blocks": [
{"type": "@theme"}, /* Accept all theme blocks */
{"type": "@app"} /* Accept app blocks */
],
"max_blocks": 16,
"presets": [
{
"name": "Preset Name",
"settings": {},
"blocks": []
}
]
}
{% endschema %}
Text Inputs:
{"type": "text", "id": "heading", "label": "Heading", "placeholder": "Enter text"}
{"type": "textarea", "id": "description", "label": "Description"}
{"type": "richtext", "id": "content", "label": "Content"}
Media Inputs:
{"type": "image_picker", "id": "image", "label": "Image"}
{"type": "video", "id": "video_file", "label": "Video"}
Choice Inputs:
{"type": "checkbox", "id": "enable", "label": "Enable", "default": true}
{"type": "select", "id": "layout", "label": "Layout", "options": [...], "default": "center"}
{"type": "radio", "id": "style", "label": "Style", "options": [...], "default": "boxed"}
Number Inputs:
{"type": "range", "id": "padding", "label": "Padding", "min": 0, "max": 200, "step": 10, "unit": "px", "default": 60}
{"type": "number", "id": "max_items", "label": "Max Items", "default": 4}
| Type | default Works? | Alternative |
|---|---|---|
text | ❌ NO | placeholder |
textarea | ❌ NO | placeholder |
url | ⚠️ INCONSISTENT | Liquid | default: |
checkbox | ✅ YES | - |
select | ✅ YES | - |
radio | ✅ YES | - |
range | ✅ YES | - |
number | ✅ YES | - |
SAFE:
{% comment %}
Section Name
Description of functionality
{% endcomment %}
UNSAFE:
{% comment %}
=====================================
DECORATIVE BREAKS PARSER
=====================================
{% endcomment %}
Rule: Plain text only. No decorative characters like ====, ----, ****.
Always use - for clean output:
{%- if condition -%}
{%- assign var = value -%}
{%- endif -%}
NEVER DO THIS:
{%- assign blog = blogs[handle | default: 'news'] -%}
Error: Expected close_square but found pipe
ALWAYS DO THIS:
{%- assign h = handle | default: 'news' -%}
{%- assign blog = blogs[h] -%}
Why: Liquid does NOT allow pipe filters inside [] bracket notation.
{%- assign categories = shop.metaobjects.blog_category.values | sort: 'display_order' -%}
{%- for category in categories -%}
{{ category.title.value }} <!-- Field value -->
{{ category.icon_id.value }} <!-- Field value -->
{{ category.system.url }} <!-- System property -->
{%- endfor -%}
Critical: ALWAYS use .value for metaobject fields.
Dynamic Blocks (Merchant-Controlled):
{% content_for "blocks" %}
Static Blocks (Developer-Controlled):
{% content_for "block", type: "heading", id: "main-heading" %}
{%- for block in section.blocks -%}
<div {{ block.shopify_attributes }}>
{%- case block.type -%}
{%- when 'heading' -%}
<h2>{{ block.settings.text }}</h2>
{%- when 'text' -%}
<p>{{ block.settings.content }}</p>
{%- endcase -%}
</div>
{%- endfor -%}
CRITICAL: Always include {{ block.shopify_attributes }} for theme editor compatibility.
.section-name {
--padding-top: {{ section.settings.padding_top }}px;
--padding-bottom: {{ section.settings.padding_bottom }}px;
--bg-color: {{ section.settings.background_color }};
padding-top: var(--padding-top);
padding-bottom: var(--padding-bottom);
background: var(--bg-color);
}
/* Desktop-first approach */
@media (max-width: 1024px) { /* Tablet */ }
@media (max-width: 768px) { /* Mobile landscape */ }
@media (max-width: 640px) { /* Mobile portrait */ }
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 32px;
}
@media (max-width: 1024px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
.grid { grid-template-columns: 1fr; }
}
/blocks/block-name.liquid{"type": "@theme"}{% content_for "blocks" %}/sections/section-name.liquid<style> block with responsive CSS{% schema %} with settings and presets{{
section.settings.image
| image_url: width: 1920
| image_tag:
loading: 'lazy',
widths: '375, 550, 750, 1000, 1500, 1920',
sizes: '100vw',
class: 'image-class'
}}
{%- -%}.valuedefault on text inputs - use placeholder{{ block.shopify_attributes }}<!-- Static heading (always first) -->
{% content_for "block",
type: "heading",
id: "hero-heading",
static: true
%}
<!-- Dynamic blocks (merchant can reorder) -->
{% content_for "blocks" %}
<!-- Static CTA (always last) -->
{% content_for "block",
type: "button",
id: "hero-cta",
static: true
%}
<!-- /blocks/group.liquid -->
<div {{ block.shopify_attributes }} class="group">
{% content_for "blocks" %} <!-- Children render here -->
</div>
{% schema %}
{
"name": "Group",
"blocks": [
{"type": "@theme"} /* Accept nested blocks */
]
}
{% endschema %}
{
"blocks": [
{
"type": "@theme",
"targeting": {
"supported_blocks": ["text", "image", "button"]
}
}
]
}
Error: Unknown tag 'endcomment'
Fix: Remove decorative characters from comments
Error: Expected close_square but found pipe
Fix: Don't use filters inside [] - split into 2 lines
Error: Invalid schema: 'default' is not a valid attribute
Fix: Use placeholder for text inputs, not default
Error: Blocks not showing in theme editor
Fix: Check that block has presets in schema
Error: Dynamic sources not working Fix: Ensure setting type is compatible (text, image_picker, url, richtext)
Official Shopify Docs:
Before creating any section or block:
placeholder not default for text{{ block.shopify_attributes }}widths and sizes.valuenpx claudepluginhub vincent-laroche/hairsolutionsco-ai-toolkit --plugin shopify-liquid-designerProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.