Sections, Blocks, and Schema Design Intermediate 10 min read

Reusable Patterns with Snippets

Build a library of reusable snippet components that make your sections more maintainable and your development faster.

As you build more sections, you’ll notice the same patterns appearing again and again: product cards, price displays, buttons, icons. Instead of duplicating this code, extract it into snippets. This creates a component library that speeds up development and ensures consistency across your theme.

Sections vs Snippets: When to Use Each

Use Sections When:

  • Merchants need to customize via theme editor
  • Content appears as a distinct page section
  • Different pages need different configurations
  • The component has its own blocks

Use Snippets When:

  • The same UI pattern appears in multiple sections
  • No merchant customization is needed
  • You want consistent styling across the theme
  • The component receives data programmatically

Common Snippet Patterns

Product Card

The most common snippet in any theme:

{# snippets/product-card.liquid #}
{#
Product Card Component
@param {product} product - Product object (required)
@param {boolean} show_vendor - Display vendor name (default: false)
@param {boolean} show_rating - Display rating stars (default: false)
@param {boolean} lazy_load - Use lazy loading (default: true)
@param {string} image_ratio - 'portrait' | 'square' | 'landscape' (default: 'portrait')
#}
{%- liquid
assign show_vendor = show_vendor | default: false
assign show_rating = show_rating | default: false
assign lazy_load = lazy_load | default: true
assign image_ratio = image_ratio | default: 'portrait'
assign loading = 'lazy'
if lazy_load == false
assign loading = 'eager'
endif
-%}
<article class="product-card">
<a href="{{ product.url }}" class="product-card__link">
<div class="product-card__media product-card__media--{{ image_ratio }}">
{%- if product.featured_image -%}
{{ product.featured_image | image_url: width: 600 | image_tag:
loading: loading,
class: 'product-card__image',
alt: product.featured_image.alt | escape
}}
{%- else -%}
{{ 'product-1' | placeholder_svg_tag: 'product-card__placeholder' }}
{%- endif -%}
{%- if product.compare_at_price > product.price -%}
<span class="product-card__badge product-card__badge--sale">Sale</span>
{%- elsif product.available == false -%}
<span class="product-card__badge product-card__badge--sold-out">Sold Out</span>
{%- endif -%}
</div>
<div class="product-card__info">
{%- if show_vendor and product.vendor != blank -%}
<p class="product-card__vendor">{{ product.vendor }}</p>
{%- endif -%}
<h3 class="product-card__title">{{ product.title }}</h3>
{%- if show_rating -%}
{% render 'rating-stars', rating: product.metafields.reviews.rating %}
{%- endif -%}
{% render 'price', product: product %}
</div>
</a>
</article>

Usage:

{% render 'product-card', product: product %}
{% render 'product-card',
product: product,
show_vendor: true,
image_ratio: 'square'
%}
{% render 'product-card' for collection.products as product %}

Price Display

{# snippets/price.liquid #}
{#
Price Display Component
@param {product} product - Product object (required)
@param {boolean} show_compare - Show compare-at price (default: true)
#}
{%- liquid
assign show_compare = show_compare | default: true
assign on_sale = false
if product.compare_at_price > product.price
assign on_sale = true
endif
-%}
<div class="price {% if on_sale %}price--on-sale{% endif %}">
{%- if on_sale and show_compare -%}
<span class="price__compare">
<span class="visually-hidden">Regular price</span>
<s>{{ product.compare_at_price | money }}</s>
</span>
{%- endif -%}
<span class="price__current">
{%- if on_sale -%}
<span class="visually-hidden">Sale price</span>
{%- endif -%}
{{ product.price | money }}
</span>
{%- if product.price_varies -%}
<span class="price__suffix">from</span>
{%- endif -%}
</div>

Button Component

{# snippets/button.liquid #}
{#
Button Component
@param {string} label - Button text (required)
@param {string} url - Link URL (renders as <a> if provided)
@param {string} style - 'primary' | 'secondary' | 'outline' (default: 'primary')
@param {string} size - 'small' | 'medium' | 'large' (default: 'medium')
@param {boolean} full_width - Full width button (default: false)
@param {string} type - 'button' | 'submit' (default: 'button', for non-links)
@param {boolean} disabled - Disabled state (default: false)
@param {string} icon - Icon name to display (optional)
#}
{%- liquid
assign style = style | default: 'primary'
assign size = size | default: 'medium'
assign full_width = full_width | default: false
assign type = type | default: 'button'
assign disabled = disabled | default: false
-%}
{%- capture classes -%}
button
button--{{ style }}
button--{{ size }}
{%- if full_width %} button--full-width{% endif -%}
{%- endcapture -%}
{%- if url != blank -%}
<a href="{{ url }}" class="{{ classes | strip_newlines | strip }}">
{%- if icon -%}
{% render 'icon', name: icon %}
{%- endif -%}
{{ label }}
</a>
{%- else -%}
<button
type="{{ type }}"
class="{{ classes | strip_newlines | strip }}"
{% if disabled %}disabled{% endif %}
>
{%- if icon -%}
{% render 'icon', name: icon %}
{%- endif -%}
{{ label }}
</button>
{%- endif -%}

Usage:

{% render 'button', label: 'Shop Now', url: '/collections/all' %}
{% render 'button', label: 'Add to Cart', type: 'submit', style: 'primary' %}
{% render 'button', label: 'Learn More', url: page.url, style: 'outline', icon: 'arrow-right' %}

Icon Component

{# snippets/icon.liquid #}
{#
SVG Icon Component
@param {string} name - Icon name (required)
@param {number} size - Size in pixels (default: 24)
@param {string} class - Additional CSS classes
#}
{%- liquid
assign icon_size = size | default: 24
-%}
<svg
class="icon icon--{{ name }} {{ class }}"
width="{{ icon_size }}"
height="{{ icon_size }}"
aria-hidden="true"
focusable="false"
>
<use href="#icon-{{ name }}"></use>
</svg>

Define icons in your layout:

{# In theme.liquid or a dedicated snippet #}
{% render 'icon-symbols' %}
{# snippets/icon-symbols.liquid #}
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="icon-cart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="9" cy="21" r="1"/>
<circle cx="20" cy="21" r="1"/>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/>
</symbol>
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</symbol>
<symbol id="icon-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="3" y1="12" x2="21" y2="12"/>
<line x1="3" y1="6" x2="21" y2="6"/>
<line x1="3" y1="18" x2="21" y2="18"/>
</symbol>
<symbol id="icon-close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</symbol>
<symbol id="icon-arrow-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</symbol>
</svg>

Image Component

{# snippets/responsive-image.liquid #}
{#
Responsive Image Component
@param {image} image - Image object (required)
@param {string} alt - Alt text (falls back to image.alt)
@param {string} sizes - Sizes attribute for responsive images
@param {boolean} lazy - Lazy load (default: true)
@param {string} class - Additional CSS classes
#}
{%- liquid
assign alt_text = alt | default: image.alt | escape
assign lazy = lazy | default: true
assign loading = 'lazy'
if lazy == false
assign loading = 'eager'
endif
assign default_sizes = '(min-width: 1200px) 1200px, 100vw'
assign sizes_attr = sizes | default: default_sizes
-%}
{%- if image != blank -%}
<img
src="{{ image | image_url: width: 800 }}"
srcset="
{{ image | image_url: width: 400 }} 400w,
{{ image | image_url: width: 600 }} 600w,
{{ image | image_url: width: 800 }} 800w,
{{ image | image_url: width: 1000 }} 1000w,
{{ image | image_url: width: 1200 }} 1200w
"
sizes="{{ sizes_attr }}"
alt="{{ alt_text }}"
loading="{{ loading }}"
width="{{ image.width }}"
height="{{ image.height }}"
{% if class %}class="{{ class }}"{% endif %}
>
{%- endif -%}
{# snippets/social-share.liquid #}
{#
Social Sharing Links
@param {string} url - URL to share (required)
@param {string} title - Share title (required)
@param {string} image - Image URL for Pinterest (optional)
#}
{%- liquid
assign encoded_url = url | url_encode
assign encoded_title = title | url_encode
-%}
<div class="social-share">
<span class="social-share__label">Share:</span>
<a
href="https://www.facebook.com/sharer/sharer.php?u={{ encoded_url }}"
class="social-share__link"
target="_blank"
rel="noopener noreferrer"
aria-label="Share on Facebook"
>
{% render 'icon', name: 'facebook', size: 20 %}
</a>
<a
href="https://twitter.com/intent/tweet?url={{ encoded_url }}&text={{ encoded_title }}"
class="social-share__link"
target="_blank"
rel="noopener noreferrer"
aria-label="Share on Twitter"
>
{% render 'icon', name: 'twitter', size: 20 %}
</a>
<a
href="https://pinterest.com/pin/create/button/?url={{ encoded_url }}&description={{ encoded_title }}{% if image %}&media={{ image | url_encode }}{% endif %}"
class="social-share__link"
target="_blank"
rel="noopener noreferrer"
aria-label="Share on Pinterest"
>
{% render 'icon', name: 'pinterest', size: 20 %}
</a>
<a
href="mailto:?subject={{ encoded_title }}&body={{ encoded_url }}"
class="social-share__link"
aria-label="Share via Email"
>
{% render 'icon', name: 'mail', size: 20 %}
</a>
</div>

Organizing Your Snippets

Since Shopify’s snippets/ folder is flat, use prefixes to organize:

snippets/
├── card-product.liquid
├── card-collection.liquid
├── card-article.liquid
├── component-button.liquid
├── component-icon.liquid
├── component-price.liquid
├── component-quantity.liquid
├── form-newsletter.liquid
├── form-contact.liquid
├── icon-symbols.liquid
├── layout-pagination.liquid
├── layout-breadcrumbs.liquid
└── util-responsive-image.liquid

Common prefixes:

  • card-: Card components
  • component-: UI components
  • form-: Form elements
  • icon-: Icon-related
  • layout-: Layout helpers
  • util-: Utility snippets

Documenting Snippets

Always document parameters at the top of each snippet:

{#
Component Name
Brief description of what it does.
@param {type} name - Description (required/optional, default: value)
@param {product} product - Product object (required)
@param {boolean} show_vendor - Show vendor name (default: false)
@param {string} size - 'small' | 'medium' | 'large' (default: 'medium')
@example
{% render 'component-name', product: product, show_vendor: true %}
#}

Using Snippets in Sections

A section using multiple snippets:

{# sections/featured-collection.liquid #}
<section class="featured-collection">
<div class="container">
<h2>{{ section.settings.heading }}</h2>
<div class="product-grid">
{%- for product in section.settings.collection.products limit: section.settings.limit -%}
{% render 'card-product',
product: product,
show_vendor: section.settings.show_vendor,
show_rating: section.settings.show_rating,
image_ratio: section.settings.image_ratio,
lazy_load: forloop.index > 4
%}
{%- endfor -%}
</div>
{%- if section.settings.show_view_all -%}
{% render 'component-button',
label: section.settings.view_all_text,
url: section.settings.collection.url,
style: 'secondary'
%}
{%- endif -%}
</div>
</section>

Avoiding Over-Abstraction

Don’t create snippets for everything. A good rule of thumb:

Create a snippet when:

  • The pattern appears 3+ times
  • It’s complex enough to benefit from isolation
  • It has clear inputs and outputs

Don’t create a snippet when:

  • It’s used once
  • It’s just a few lines
  • It makes the code harder to follow
{# Too granular - just use inline code #}
{% render 'heading', text: section.settings.heading, tag: 'h2' %}
{# Just write it #}
<h2>{{ section.settings.heading }}</h2>

Practice Exercise

Create a card-collection.liquid snippet that displays:

  1. Collection image (with placeholder fallback)
  2. Collection title
  3. Product count
  4. Optional “Shop now” button
{# snippets/card-collection.liquid #}
{#
Collection Card Component
@param {collection} collection - Collection object (required)
@param {boolean} show_count - Show product count (default: true)
@param {boolean} show_button - Show CTA button (default: false)
@param {string} button_text - Button text (default: 'Shop Now')
#}
{%- liquid
assign show_count = show_count | default: true
assign show_button = show_button | default: false
assign button_text = button_text | default: 'Shop Now'
-%}
<article class="collection-card">
<a href="{{ collection.url }}" class="collection-card__link">
<div class="collection-card__media">
{%- if collection.image -%}
{{ collection.image | image_url: width: 600 | image_tag:
loading: 'lazy',
class: 'collection-card__image'
}}
{%- else -%}
{{ 'collection-1' | placeholder_svg_tag: 'collection-card__placeholder' }}
{%- endif -%}
</div>
<div class="collection-card__info">
<h3 class="collection-card__title">{{ collection.title }}</h3>
{%- if show_count -%}
<p class="collection-card__count">
{{ collection.all_products_count }}
{{ collection.all_products_count | pluralize: 'product', 'products' }}
</p>
{%- endif -%}
</div>
</a>
{%- if show_button -%}
{% render 'component-button',
label: button_text,
url: collection.url,
style: 'secondary',
size: 'small'
%}
{%- endif -%}
</article>

Key Takeaways

  1. Extract repeated patterns into snippets
  2. Document parameters at the top of each snippet
  3. Provide sensible defaults for optional parameters
  4. Use consistent prefixes to organize snippets
  5. Snippets call other snippets to build complex UIs
  6. Don’t over-abstract simple code
  7. Snippets have isolated scope with render

What’s Next?

Congratulations! You’ve completed Module 5: Sections, Blocks, and Schema Design. You now know how to build powerful, customizable section components with well-organized settings and reusable snippet patterns.

In the next module, you’ll learn about Assets: CSS, JS, and Front-End Architecture for styling and adding interactivity to your theme.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...