Develop your theme focused on scalability and reusability
# How to Create Reusable Shopify Snippets
A reference for building scalable, easy-to-use Liquid snippets. Use these patterns when creating heading, CTA, announcement, image, and other reusable components.
## When to Create a Snippet
Create a snippet when:
- The same UI appears in 3+ sections or templates
- Logic is complex enough to warrant isolation
- You need consistency (e.g. all CTAs look the same)
- Schema settings would be duplicated across sections
Avoid snippets for:
- One-off layouts
- Content that varies wildly by context
- Simple output (< 5 lines)
## Snippet Documentation Format
Use structured `{% comment %}` blocks at the top of every snippet:
```liquid
{% comment %}
===================================================================================
SNIPPET NAME
===================================================================================
Brief description of what the snippet does and when to use it.
********************************************
Parameters
********************************************
* param_name (Type): Description
* optional_param (Type, Optional): Description. Default: value
********************************************
Usage
********************************************
{% render 'snippet-name', param: value, optional: value %}
********************************************
Schema (for section settings that pass into snippet)
********************************************
{ "type": "text", "id": "param_id", "label": "Label", "visible_if": "{{ section.settings.enable }}" }
{% endcomment %}
```
## Parameter Naming Conventions
### Kebab-case for Multi-word Parameters
Use kebab-case when the parameter name has multiple words:
```liquid
{% render 'section-heading'
title: ss.heading_title,
sub_text: ss.heading_sub_text,
align: ss.heading_align,
align_mb: ss.heading_align_mb,
shop_all: ss.heading_shop_all
%}
```
### Action-based Select (vs Multiple Booleans)
For mutually exclusive behaviors, use a single `action` param instead of multiple booleans:
```liquid
{% render 'cta-button'
text: ss.cta_text,
link: product.url,
type: ss.cta_type,
action: ss.cta_action,
variant_id: product.selected_or_first_available_variant.id,
note: ss.cta_note,
note_icon: ss.cta_note_icon
%}
```
Schema: `action` select with options like `""` (link), `"atc"` (add to cart), `"scroll"` (scroll to product).
### Consistent Parameter Order
Order parameters by: enable flag → content → styling → optional extras. Keeps calls readable.
## Conditional Rendering
### Wrapper Conditionals
Wrap the entire output when the snippet may render nothing:
```liquid
{% if enable %}
<div class="section-heading">
<!-- Content -->
</div>
{% endif %}
```
### Element-level Conditionals
Check blanks before rendering individual elements:
```liquid
{% if title != blank %}
<h2 class="section-heading__title">{{ title }}</h2>
{% endif %}
{% if text != blank %}
<div class="section-heading__text">{{ text }}</div>
{% endif %}
```
### Early Skip
For complex snippets, wrap everything in a single top-level conditional:
```liquid
{% if enable and (title != blank or text != blank) %}
<div class="section-heading">
<!-- Content -->
</div>
{% endif %}
```
Liquid has no early return; use a single wrapping conditional instead.
## Variable Assignments
### Default Values
Use `default` filter for fallbacks:
```liquid
{% assign heading_tag = h1 | default: false %}
{% assign heading_tag = heading_tag | replace: 'true', '1' | replace: 'false', '2' | prepend: 'h' %}
```
### Class Concatenation
Build class strings from parameters:
```liquid
{% assign classes = variant | append: ' ' | append: class | strip %}
<div class="cta-button {{ classes }}">
```
### Liquid Block for Multiple Assignments
Use `{% liquid %}` for cleaner multi-step logic:
```liquid
{% liquid
assign product = product | default: section.settings.product
assign link = link | default: product.url
assign variant_id = variant_id | default: product.selected_or_first_available_variant.id
%}
```
### String Manipulation for Derived Values
Extract link title from URL for `title` attribute:
```liquid
{% assign link_title = link | split: '?' | first | split: '//' | last | split: '/' | slice: 1, 2 | join: ' ' | capitalize %}
```
## Dynamic HTML Elements
### Dynamic Tag Names
Use variables for tag names (e.g. h1 vs h2):
```liquid
{% assign tag = h1 | default: false | replace: 'true', '1' | replace: 'false', '2' | prepend: 'h' %}
<{{ tag }} class="section-heading__title">{{ title }}</{{ tag }}>
```
### Configurable Element Type
Allow link vs button based on context:
```liquid
{% assign el = element | default: 'a' %}
<{{ el }} href="{{ link }}" class="cta-button">
{{ text }}
</{{ el }}>
```
## Multiple Rendering Modes
### Action-based Branching
Use `action` param for mutually exclusive behaviors. Keeps schema simple (one select vs multiple checkboxes):
```liquid
{% if scroll or action == 'scroll' %}
<div class="cta-button {{ type }}" data-scroll-to="product" aria-label="Scroll to product">
{% if text != blank %}{{ text }}{% else %}{{ 'products.product.shop_now' | t }}{% endif %}
</div>
{% elsif action == 'atc' and variant_id != blank %}
<div class="cta-button {{ type }}" data-variant-id="{{ variant_id }}" aria-label="Add to Cart">
{% if text != blank %}{{ text }}{% else %}{{ 'products.product.add_to_cart' | t }}{% endif %}
</div>
{% else %}
<a href="{% if link != blank %}{{ link }}{% else %}/{% endif %}" title="{{ link_title }}" class="cta-button {{ type }}">
{% if text != blank %}{{ text }}{% else %}{{ 'products.product.shop_now' | t }}{% endif %}
</a>
{% endif %}
```
Add-to-cart uses a `div` with `data-variant-id`; JavaScript handles the form submission. No `<form>` in snippet.
### Captured Auxiliary Content
Use `{% capture %}` for optional content reused across all modes (e.g. CTA note below button):
```liquid
{% capture cta_note %}
{% if note != blank %}
<div class="cta-note">
{% if note_icon != blank %}
{{ note_icon | image_url: width: 30 | image_tag: loading: 'lazy', alt: '' }}
{% endif %}
<p>{{ note }}</p>
</div>
{% endif %}
{% endcapture %}
{% if action == 'scroll' %}
<div class="cta-button {{ type }}">...</div>
{{ cta_note }}
{% elsif action == 'atc' %}
<div class="cta-button {{ type }}">...</div>
{{ cta_note }}
{% else %}
<a href="{{ link }}" class="cta-button {{ type }}">...</a>
{{ cta_note }}
{% endif %}
```
### Translation Fallbacks
Use locale key when text is blank:
```liquid
{% if text != blank %}{{ text }}{% else %}{{ 'products.product.add_to_cart' | t }}{% endif %}
```
## Nested Snippet Rendering
### Conditional Nesting
Render child snippets only when relevant:
```liquid
{% if show_ratings %}
{% if ratings_type == 'custom' %}
{% render 'ratings-custom', stars: ratings_stars, logo: ratings_logo, text: ratings_text %}
{% else %}
{% render 'ratings-widget', type: ratings_type %}
{% endif %}
{% endif %}
```
### Pass-through Parameters
Forward all needed params to nested snippets. Avoid passing whole objects when a few values suffice:
```liquid
{% render 'countdown-timer'
enabled: timer,
type: timer_type,
end_date: timer_date,
timezone: timer_timezone,
format: timer_format,
class: 'section-heading__timer'
%}
```
## Block Iteration
When sections pass blocks into a snippet:
```liquid
{% if items.size > 0 %}
<div class="section-heading__items">
{% for item in items %}
<div class="section-heading__item" {{ item.shopify_attributes }}>
{% if item.settings.icon %}
{{ item.settings.icon | image_url: width: 60 | image_tag: loading: 'lazy', alt: item.settings.text }}
{% endif %}
<p>{{ item.settings.text }}</p>
</div>
{% endfor %}
</div>
{% endif %}
```
## Data Attributes for JavaScript
Use data attributes instead of inline scripts:
```liquid
<div class="countdown-timer {{ class }}"
data-type="{{ type }}"
data-end-date="{{ end_date }}"
{% if timezone %}data-timezone="{{ timezone }}"{% endif %}
{% if format %}data-format="{{ format }}"{% endif %}>
<span class="countdown-timer__display">00:00:00</span>
</div>
```
JavaScript reads these and initializes behavior. Keeps snippet portable.
## Inline Styles for Dynamic Values
Use inline styles for colors/sizes passed from schema:
```liquid
<div class="announcement-bar" style="background-color: {{ bg_color }}; color: {{ text_color }};">
<!-- Content -->
</div>
```
## Case Statements
Use `case` for multiple variants:
```liquid
{% case variant %}
{% when 'accent' %}
{% assign btn_class = 'cta-button--accent' %}
{% when 'outline' %}
{% assign btn_class = 'cta-button--outline' %}
{% else %}
{% assign btn_class = 'cta-button--primary' %}
{% endcase %}
```
## Class Variants
Support variants via parameter concatenation:
```liquid
{% assign classes = variant | append: ' ' | append: class | strip %}
<div class="cta-button {{ classes }}">
```
Conditional modifier:
```liquid
<div class="section-heading__text{% if read_more %} section-heading__text--expandable{% endif %}">{{ text }}</div>
```
## Object Access
Pass objects when the snippet needs them. Derive values with fallbacks:
```liquid
{% liquid
assign product = product | default: section.settings.product
assign link = link | default: product.url
assign variant_id = variant_id | default: product.selected_or_first_available_variant.id
%}
```
For collection links:
```liquid
{% if shop_all != blank %}
<a href="{{ shop_all.url }}" class="section-heading__shop-all">{{ shop_all.title }}</a>
{% endif %}
```
## Icon and Image Rendering
Conditional icon:
```liquid
{% if show_icon %}{% render 'icon-arrow' %}{% endif %}
```
If your theme has an image snippet, use it. Otherwise use built-in filters:
```liquid
{% if image %}
{% assign img_width = width | default: 400 %}
{{ image | image_url: width: img_width | image_tag: loading: 'lazy', alt: alt | default: '' }}
{% endif %}
```
## Snippet Types (Reference Examples)
### Heading Snippet
- **Purpose**: Reusable section heading (title, subtitle, alignment, optional link)
- **Params**: `title`, `sub_text`, `align`, `align_mb`, `shop_all`, `h1`, `class`
- **Pattern**: Wrapper conditional, dynamic tag, element-level blanks
### CTA Snippet
- **Purpose**: Links, scroll-to-product, or add-to-cart with optional note
- **Params**: `text`, `link`, `type` (style variant), `action` (`""` | `"atc"` | `"scroll"`), `variant_id`, `product` (fallback for link/variant_id), `note`, `note_icon`
- **Pattern**: Action-based branching, `{% capture %}` for note, translation fallbacks, data attributes for ATC/scroll (JS handles submit)
- **Add-to-cart**: Use `div` with `data-variant-id`; theme JS intercepts and submits AJAX form
### Announcement Bar Snippet
- **Purpose**: Top bar with text, optional timer, colors from schema
- **Params**: `text`, `bg_color`, `text_color`, `timer`, `timer_*` (pass-through)
- **Pattern**: Inline styles, nested timer snippet, wrapper conditional
### Image Snippet
- **Purpose**: Consistent image output with lazy load, alt, sizes
- **Params**: `image`, `width`, `alt`, `loading`, `class`
- **Pattern**: Blank check, default width, optional class
### Countdown/Timer Snippet
- **Purpose**: JS-driven countdown with config from data attributes
- **Params**: `type`, `end_date`, `timezone`, `format`, `class`
- **Pattern**: Data attributes, no inline JS, placeholder display text
## Schema Integration
When sections use snippets, schema should mirror snippet params:
1. **Header** to group related settings
2. **Checkbox** for enable/disable
3. **Text/richtext** for content
4. **Select** for variant/type/action
5. **Color** for dynamic colors
6. **URL** for links
7. **image_picker** for optional icons (e.g. CTA note icon)
8. **visible_if** – **Required on every setting** that depends on an enable/parent checkbox
### CTA Schema Example
```json
{
"type": "header",
"content": "CTA"
},
{
"type": "checkbox",
"id": "show_cta",
"label": "Show CTA",
"default": true
},
{
"type": "select",
"id": "cta_action",
"label": "CTA Action",
"default": "atc",
"visible_if": "{{ section.settings.show_cta }}",
"options": [
{ "value": "", "label": "Link" },
{ "value": "atc", "label": "Add to Cart" },
{ "value": "scroll", "label": "Scroll to Product" }
]
},
{
"type": "select",
"id": "cta_type",
"label": "CTA Style",
"default": "accent",
"visible_if": "{{ section.settings.show_cta }}",
"options": [
{ "value": "light", "label": "Light" },
{ "value": "dark", "label": "Dark" },
{ "value": "accent", "label": "Accent" }
]
},
{
"type": "text",
"id": "cta_text",
"label": "CTA Text",
"default": "Add to Cart",
"visible_if": "{{ section.settings.show_cta }}"
},
{
"type": "text",
"id": "cta_note",
"label": "CTA Note",
"visible_if": "{{ section.settings.show_cta }}"
},
{
"type": "image_picker",
"id": "cta_note_icon",
"label": "CTA Note Icon",
"visible_if": "{{ section.settings.show_cta }}"
}
```
### Heading Schema Example
```json
{
"type": "header",
"content": "Heading"
},
{
"type": "checkbox",
"id": "heading",
"label": "Show Heading",
"default": true
},
{
"type": "text",
"id": "heading_title",
"label": "Title",
"visible_if": "{{ section.settings.heading }}"
},
{
"type": "text",
"id": "heading_sub_text",
"label": "Subtitle",
"visible_if": "{{ section.settings.heading }}"
},
{
"type": "select",
"id": "heading_align",
"label": "Alignment",
"default": "center",
"visible_if": "{{ section.settings.heading }}",
"options": [
{ "value": "start", "label": "Start" },
{ "value": "center", "label": "Center" },
{ "value": "end", "label": "End" }
]
}
```
## Best Practices
1. **Wrap in conditionals** when snippet might output nothing
2. **Use kebab-case** for multi-word parameters
3. **Provide defaults** with `default` filter (`product | default: section.settings.product`)
4. **Check blank** before rendering content
5. **Document params** in structured comment blocks
6. **Use action select** instead of multiple booleans for mutually exclusive modes
7. **Use `{% capture %}`** for auxiliary content (notes, badges) reused across branches
8. **Use data attributes** for JS (ATC, scroll); keep snippets free of inline scripts
9. **Use translation fallbacks** when text is blank: `{% if text != blank %}{{ text }}{% else %}{{ 'key' | t }}{% endif %}`
10. **Support class/variant params** for styling flexibility
11. **Use inline styles** only for schema-driven values (colors, etc.)
12. **Use `case`** for 3+ conditional branches
13. **Keep snippets focused** – one clear responsibility per file
14. **Avoid deep nesting** – 2–3 levels max; extract if deeper
15. **Name consistently** – `section-heading`, `cta-button`, `announcement-bar`
16. **Add visible_if to every setting** – All settings under an enable checkbox must have `visible_if` referencing that checkbox