Storetools.

IV · Reference

Liquid worth stealing.

50 copy-paste snippets for Shopify themes — each one self-contained, commented, and written for current Online Store 2.0. Press / to search.

Products

Product Recommendations

Displays data-driven related products fetched from the recommendations endpoint and refreshed via the Section Rendering API.

{% comment %}
  Product recommendations. Save as sections/product-recommendations.liquid
  and render it on the product page. On first paint Shopify has no
  recommendations, so the wrapper is refreshed by JS calling the
  Section Rendering API, which repopulates recommendations.products.
{% endcomment %}
<div
  class="st-recommendations"
  data-recommendations
  data-product-id="{{ product.id }}"
  data-section-id="{{ section.id }}"
  data-limit="4">
  {% if recommendations.performed and recommendations.products_count > 0 %}
    <h2 class="st-recommendations__title">You may also like</h2>
    <ul class="st-recommendations__grid" role="list">
      {% for item in recommendations.products %}
        <li class="st-recommendations__card">
          <a href="{{ item.url }}">
            {% if item.featured_image %}
              <img src="{{ item.featured_image | image_url: width: 300 }}"
                   alt="{{ item.featured_image.alt | escape }}"
                   width="300" height="300" loading="lazy">
            {% endif %}
            <span class="st-recommendations__name">{{ item.title }}</span>
            <span class="st-recommendations__price">{{ item.price | money }}</span>
          </a>
        </li>
      {% endfor %}
    </ul>
  {% endif %}
</div>
<script>
  (function () {
    var el = document.querySelector('[data-recommendations]');
    if (!el || el.dataset.loaded) return;
    var url = '{{ routes.product_recommendations_url }}?product_id=' + el.dataset.productId +
      '&limit=' + el.dataset.limit + '&section_id=' + el.dataset.sectionId;
    fetch(url).then(function (r) { return r.text(); }).then(function (html) {
      var next = new DOMParser().parseFromString(html, 'text/html').querySelector('[data-recommendations]');
      if (next) { el.innerHTML = next.innerHTML; el.dataset.loaded = 'true'; }
    });
  })();
</script>

Preselected variant image

Shows the image of the selected (or first available) variant, falling back to the product's featured image.

{% comment %}
  Preselected variant image: honours ?variant= deep links by showing the
  selected variant's image, falling back to the product's featured image.
  Paste into your product template or main-product section.
{% endcomment %}
{% assign st_variant = product.selected_or_first_available_variant %}
{% assign st_image = st_variant.featured_media.preview_image | default: product.featured_image %}

<div class="st-variant-image">
  {% if st_image %}
    <a href="{{ st_image | image_url: width: 1500 }}" class="st-variant-image__link">
      <img src="{{ st_image | image_url: width: 1024 }}"
           srcset="{{ st_image | image_url: width: 512 }} 512w, {{ st_image | image_url: width: 1024 }} 1024w, {{ st_image | image_url: width: 1500 }} 1500w"
           sizes="(min-width: 750px) 50vw, 100vw"
           width="{{ st_image.width }}"
           height="{{ st_image.height }}"
           alt="{{ st_image.alt | default: product.title | escape }}"
           loading="eager"
           class="st-variant-image__img">
    </a>
  {% else %}
    {{ 'product-1' | placeholder_svg_tag: 'st-variant-image__img st-variant-image__img--placeholder' }}
  {% endif %}
</div>

Display product metafields list

Renders every metafield in a chosen namespace as a labelled key and value list.

{% comment %}
  Display product metafields from a single namespace.
  Paste into a product template or product section.
  Set the namespace below to match the one you created in the Shopify admin
  (Settings > Custom data > Products).
{% endcomment %}
{%- assign field_group = product.metafields.details -%}
{%- if field_group != blank -%}
  <dl class="st-metafields">
    {%- for entry in field_group -%}
      {%- assign field_key = entry | first -%}
      {%- assign field_value = entry | last -%}
      {%- if field_value != blank -%}
        <div class="st-metafields__row">
          <dt class="st-metafields__label">{{ field_key | replace: '_', ' ' | capitalize }}</dt>
          <dd class="st-metafields__value">{{ field_value }}</dd>
        </div>
      {%- endif -%}
    {%- endfor -%}
  </dl>
{%- endif -%}

Variant option selector

Builds a labelled dropdown for each product option so customers can pick a variant.

{% comment %}
  Variant option selector.
  Paste inside the product form on a product template or section.
  Renders one dropdown per option (size, colour, etc.) and marks the
  currently selected values as chosen.
{% endcomment %}
{%- unless product.has_only_default_variant -%}
  <div class="st-variant-picker">
    {%- for option in product.options_with_values -%}
      <div class="st-variant-picker__field">
        <label class="st-variant-picker__label" for="st-option-{{ forloop.index }}">
          {{ option.name | escape }}
        </label>
        <select
          id="st-option-{{ forloop.index }}"
          class="st-variant-picker__select"
          name="options[{{ option.name | escape }}]"
        >
          {%- for value in option.values -%}
            <option
              value="{{ value | escape }}"
              {% if option.selected_value == value %}selected{% endif %}
            >
              {{ value | escape }}
            </option>
          {%- endfor -%}
        </select>
      </div>
    {%- endfor -%}
  </div>
{%- endunless -%}

Show product SKU

Outputs the SKU of the selected or first available variant when one is set.

{% comment %}
  Show the product SKU.
  Paste into a product template or section, near the title or price.
  Reads the selected variant so it can be refreshed by the theme's variant JS.
{% endcomment %}
{%- assign shown_variant = product.selected_or_first_available_variant -%}
{%- if shown_variant.sku != blank -%}
  <p class="st-sku">
    <span class="st-sku__label">SKU:</span>
    <span class="st-sku__value" data-variant-sku>{{ shown_variant.sku }}</span>
  </p>
{%- endif -%}

Single variant product form

Renders a buy form for products that have one variant, with a sold-out fallback.

{% comment %}
  Single variant product form.
  Paste into a product template or section for items that have only one variant.
  Hides the variant picker and disables the button when the item is sold out.
{% endcomment %}
{%- assign only_variant = product.selected_or_first_available_variant -%}
{% form 'product', product, class: 'st-buy-form' %}
  <input type="hidden" name="id" value="{{ only_variant.id }}">

  <p class="st-buy-form__price">{{ only_variant.price | money }}</p>

  {%- if only_variant.available -%}
    <button type="submit" name="add" class="st-buy-form__submit">
      Add to cart
    </button>
    {{ form | payment_button }}
  {%- else -%}
    <button type="submit" name="add" class="st-buy-form__submit" disabled>
      Sold out
    </button>
  {%- endif -%}
{% endform %}

Percent-off sale badge

A small badge with the discount percentage, shown only when a product is actually reduced.

{% comment %}
  Percent-off badge — paste inside a product card; expects a product object.
{% endcomment %}
{% if product.compare_at_price > product.price %}
  {% assign st_saving = product.compare_at_price | minus: product.price %}
  {% assign st_pct = st_saving | times: 100 | divided_by: product.compare_at_price %}
  <span class="st-badge">&minus;{{ st_pct }}%</span>
{% endif %}

Low-stock note

An honest urgency line that appears only when the selected variant is nearly out.

{% comment %}
  Low-stock note — product template; shows only when the variant tracks
  inventory and five or fewer remain.
{% endcomment %}
{% assign st_variant = product.selected_or_first_available_variant %}
{% if st_variant.inventory_management == 'shopify'
   and st_variant.inventory_quantity > 0
   and st_variant.inventory_quantity <= 5 %}
  <p class="st-low-stock">Only {{ st_variant.inventory_quantity }} left in stock.</p>
{% endif %}

Estimated delivery window

A delivery date range computed from today, formatted like a human wrote it.

{% comment %}
  Estimated delivery window — product template.
  259200 = 3 days out, 604800 = 7 days out; adjust to your carrier.
{% endcomment %}
{% assign st_from = 'now' | date: '%s' | plus: 259200 %}
{% assign st_to = 'now' | date: '%s' | plus: 604800 %}
<p class="st-delivery">
  Estimated delivery: {{ st_from | date: '%b %-d' }} &ndash; {{ st_to | date: '%b %-d' }}
</p>

Spec table from metafields

Turns every metafield in a `specs` namespace into a tidy specification table.

{% comment %}
  Spec table — product template. Create single-line text metafields under
  the namespace "specs" (e.g. specs.material, specs.weight) and they all
  render here, no theme edits per field.
{% endcomment %}
{% if product.metafields.specs != blank %}
  <table class="st-specs">
    {% for st_field in product.metafields.specs %}
      <tr>
        <th>{{ st_field[0] | replace: '_', ' ' | capitalize }}</th>
        <td>{{ st_field[1].value }}</td>
      </tr>
    {% endfor %}
  </table>
{% endif %}

Collections

Collection List

Displays every store collection as a linked card with an image that falls back to the first product's photo.

{% comment %}
  Collection grid. Use on a list-collections template/section,
  or anywhere the global collections object is available.
{% endcomment %}
<ul class="st-collection-list">
  {% for collection in collections %}
    {% assign card_image = collection.image %}
    {% if card_image == blank and collection.products.first %}
      {% assign card_image = collection.products.first.featured_image %}
    {% endif %}
    <li class="st-collection-list__item">
      <a class="st-collection-list__link" href="{{ collection.url }}">
        <div class="st-collection-list__media">
          {% if card_image %}
            {{ card_image | image_url: width: 480 | image_tag: alt: collection.title, loading: 'lazy' }}
          {% endif %}
        </div>
        <h3 class="st-collection-list__title">{{ collection.title }}</h3>
      </a>
    </li>
  {% endfor %}
</ul>

Collection Product Grid

Renders a paginated grid of the current collection's products with image, title, vendor, and price.

{% comment %}
  Collection product grid. Paste into sections/main-collection.liquid
  or a collection template. Lists every product in the current
  collection with a linked image, title, vendor, and price.
{% endcomment %}
<div class="st-collection">
  <h1 class="st-collection__title">{{ collection.title }}</h1>
  {% if collection.description != blank %}
    <div class="st-collection__intro">{{ collection.description }}</div>
  {% endif %}

  {% paginate collection.products by 24 %}
    <ul class="st-collection__grid" role="list">
      {% for product in collection.products %}
        <li class="st-collection__card">
          <a href="{{ product.url }}" class="st-collection__link">
            {% if product.featured_image %}
              <img
                src="{{ product.featured_image | image_url: width: 400 }}"
                alt="{{ product.featured_image.alt | escape }}"
                width="400" height="400" loading="lazy">
            {% endif %}
            <span class="st-collection__name">{{ product.title }}</span>
            <span class="st-collection__vendor">{{ product.vendor }}</span>
            <span class="st-collection__price">
              {%- if product.price_varies -%}
                From {{ product.price_min | money }}
              {%- else -%}
                {{ product.price | money }}
              {%- endif -%}
            </span>
          </a>
        </li>
      {% endfor %}
    </ul>
    {{ paginate | default_pagination }}
  {% endpaginate %}
</div>

Collection price range

Shows a min–max price range for products with varying prices on collection pages, and a single price elsewhere.

{% comment %}
  Price range: shows a 'from–to' range on collection pages when a product's
  variants differ in price; otherwise a single price. Paste into a product card.
{% endcomment %}
<div class="st-price">
  {% if product.available %}
    {% if product.price_varies and template.name == 'collection' %}
      <span class="st-price__range">
        <span class="st-price__min">{{ product.price_min | money }}</span>
        <span class="st-price__separator" aria-hidden="true">&ndash;</span>
        <span class="st-price__max">{{ product.price_max | money }}</span>
      </span>
    {% else %}
      {% if product.price_varies %}
        <span class="st-price__prefix">From</span>
      {% endif %}
      <span class="st-price__amount">{{ product.price | money }}</span>
    {% endif %}
  {% else %}
    <span class="st-price__sold-out">Sold out</span>
  {% endif %}
</div>

Limit products per collection page

Paginates a collection so only a fixed number of products render per page.

{% comment %}
  Limit products per collection page.
  Paste inside a collection template or section where the product grid is rendered.
  Change per_page to control how many products show before pagination kicks in.
{% endcomment %}
{%- assign per_page = 12 -%}
{%- paginate collection.products by per_page -%}
  <ul class="st-collection-grid">
    {%- for product in collection.products -%}
      <li class="st-collection-grid__item">
        <a href="{{ product.url }}" class="st-collection-grid__link">
          {{ product.title }}
        </a>
      </li>
    {%- endfor -%}
  </ul>

  {%- if paginate.pages > 1 -%}
    <nav class="st-collection-grid__pagination" role="navigation" aria-label="Pagination">
      {{ paginate | default_pagination }}
    </nav>
  {%- endif -%}
{%- endpaginate -%}

Showing X–Y of Z products

The little orientation line every paginated collection deserves.

{% comment %}
  "Showing X–Y of Z" — collection template, inside your paginate block.
{% endcomment %}
{% paginate collection.products by 24 %}
  <p class="st-count">
    Showing {{ paginate.current_offset | plus: 1 }}&ndash;{{ paginate.current_offset | plus: collection.products.size }}
    of {{ collection.products_count }} products
  </p>
  {% comment %} …your product grid… {% endcomment %}
{% endpaginate %}

Cart

Cart Notes

Adds a special-instructions textarea to the cart that saves an order note on checkout.

{% comment %}
  Cart order note. Place inside the cart form block (form 'cart')
  in your cart template or section. The name="note" attribute is required.
{% endcomment %}
<div class="st-cart-note">
  <label class="st-cart-note__label" for="st-cart-note">
    Order notes
    <span class="st-cart-note__hint">Add special instructions for your order</span>
  </label>
  <textarea
    class="st-cart-note__field"
    id="st-cart-note"
    name="note"
    rows="4"
    placeholder="e.g. leave at the front desk"
  >{{ cart.note }}</textarea>
</div>

Checkout Form

Renders an editable cart form with line-item thumbnails, quantities, remove links, and a checkout button.

{% comment %}
  Cart line items and checkout button. Use in your cart
  template/section where the cart object is available.
{% endcomment %}
{% if cart.item_count > 0 %}
  {% form 'cart', cart, class: 'st-cart-form' %}
    <ul class="st-cart-form__items">
      {% for item in cart.items %}
        <li class="st-cart-form__item">
          {% if item.image %}
            <a href="{{ item.url }}">
              {{ item.image | image_url: width: 200 | image_tag: alt: item.title, loading: 'lazy' }}
            </a>
          {% endif %}
          <div class="st-cart-form__details">
            <a href="{{ item.url }}">{{ item.product.title }}</a>
            {% unless item.variant.title contains 'Default' %}
              <p class="st-cart-form__variant">{{ item.variant.title }}</p>
            {% endunless %}
            <label class="st-visually-hidden" for="qty-{{ item.index | plus: 1 }}">Quantity</label>
            <input id="qty-{{ item.index | plus: 1 }}" type="number" name="updates[]" value="{{ item.quantity }}" min="0">
            <a class="st-cart-form__remove" href="{{ item.url_to_remove }}">Remove</a>
            <span class="st-cart-form__price">{{ item.final_line_price | money }}</span>
          </div>
        </li>
      {% endfor %}
    </ul>
    <div class="st-cart-form__footer">
      <p class="st-cart-form__total">Subtotal: {{ cart.total_price | money }}</p>
      <button type="submit" name="update" class="st-cart-form__update">Update</button>
      <button type="submit" name="checkout" class="st-cart-form__checkout">Checkout</button>
    </div>
  {% endform %}
{% else %}
  <p class="st-cart-form__empty">Your cart is empty. <a href="{{ routes.all_products_collection_url }}">Continue shopping</a></p>
{% endif %}

Free-shipping progress bar

Shows how far the cart is from free shipping, with a thin progress bar.

{% comment %}
  Free-shipping progress — paste into your cart drawer or cart template.
  Set the threshold in cents (7500 = $75.00).
{% endcomment %}
{% assign st_threshold = 7500 %}
{% if cart.total_price >= st_threshold %}
  <p class="st-ship-note">Your order ships free.</p>
{% else %}
  {% assign st_remaining = st_threshold | minus: cart.total_price %}
  {% assign st_pct = cart.total_price | times: 100 | divided_by: st_threshold %}
  <p class="st-ship-note">{{ st_remaining | money }} away from free shipping.</p>
  <div class="st-ship-track" style="height:4px;background:#eee;">
    <div style="height:4px;width:{{ st_pct }}%;background:#2e7b4f;"></div>
  </div>
{% endif %}

Per-line cart savings

Shows what each cart line saves against its compare-at price.

{% comment %}
  Per-line savings — cart template, inside your line-item loop.
{% endcomment %}
{% for st_item in cart.items %}
  {% if st_item.variant.compare_at_price > st_item.variant.price %}
    {% assign st_save = st_item.variant.compare_at_price
      | minus: st_item.variant.price
      | times: st_item.quantity %}
    <span class="st-line-save">You save {{ st_save | money }}</span>
  {% endif %}
{% endfor %}

Gift-wrap checkbox

A gift-wrapping option stored as a cart attribute, visible on the order.

{% comment %}
  Gift-wrap option — place inside your cart form. The choice arrives on
  the order as a cart attribute your staff can see.
{% endcomment %}
<label class="st-giftwrap">
  <input type="checkbox" name="attributes[Gift wrap]" value="Yes"
    {% if cart.attributes['Gift wrap'] == 'Yes' %}checked{% endif %}>
  Add gift wrapping
</label>

Blog

Blog Article List

Lists a blog's articles with a featured image, byline, excerpt, tag links, and comment count.

{% comment %}
  Blog article list. Use in a blog template/section where the
  blog object is available (e.g. sections/main-blog.liquid).
{% endcomment %}
<ul class="st-article-list">
  {% for article in blog.articles %}
    <li class="st-article-list__item">
      {% if article.image %}
        <a class="st-article-list__media" href="{{ article.url }}">
          {{ article.image | image_url: width: 600 | image_tag: alt: article.title, loading: 'lazy' }}
        </a>
      {% endif %}
      <h3 class="st-article-list__title"><a href="{{ article.url }}">{{ article.title }}</a></h3>
      {% if section.settings.show_meta %}
        <p class="st-article-list__meta">
          {{ article.author }} &middot;
          <time datetime="{{ article.published_at | date: '%Y-%m-%d' }}">{{ article.published_at | date: '%B %-d, %Y' }}</time>
        </p>
      {% endif %}
      <p class="st-article-list__excerpt">
        {% if article.excerpt != blank %}{{ article.excerpt | strip_html }}{% else %}{{ article.content | strip_html | truncate: 150 }}{% endif %}
      </p>
      {% if article.tags.size > 0 %}
        <ul class="st-article-list__tags">
          {% for tag in article.tags %}<li><a href="{{ blog.url }}/tagged/{{ tag | handleize }}">{{ tag }}</a></li>{% endfor %}
        </ul>
      {% endif %}
      <a class="st-article-list__more" href="{{ article.url }}">Read more</a>
      {% if blog.comments_enabled? %}<span class="st-article-list__comments">{{ article.comments_count }} comments</span>{% endif %}
    </li>
  {% endfor %}
</ul>

Blog Article Page

Renders a single article with an optional byline and date, the body content, and a footer of tag links.

{% comment %}
  Single article layout. Use in an article template/section where
  the article object is available (e.g. sections/main-article.liquid).
{% endcomment %}
<article class="st-article">
  <header class="st-article__header">
    <h1 class="st-article__title">{{ article.title }}</h1>
    {% if section.settings.show_author or section.settings.show_date %}
      <p class="st-article__meta">
        {% if section.settings.show_author %}
          <span class="st-article__author">By {{ article.author }}</span>
        {% endif %}
        {% if section.settings.show_date %}
          <time class="st-article__date" datetime="{{ article.published_at | date: '%Y-%m-%d' }}">
            {{ article.published_at | date: '%B %-d, %Y' }}
          </time>
        {% endif %}
      </p>
    {% endif %}
  </header>

  <div class="st-article__content rte">
    {{ article.content }}
  </div>

  {% if article.tags.size > 0 %}
    <footer class="st-article__footer">
      <ul class="st-article__tags">
        {% for tag in article.tags %}
          <li><a href="{{ blog.url }}/tagged/{{ tag | handleize }}">{{ tag }}</a></li>
        {% endfor %}
      </ul>
    </footer>
  {% endif %}
</article>

Blog Tag Listing

Shows a sidebar of links for every tag in a blog, with the active tag highlighted.

{% comment %}
  Blog tag list. Place in a blog template/section where the blog
  object is available (e.g. a sidebar in sections/main-blog.liquid).
{% endcomment %}
{% if blog.all_tags.size > 0 %}
  <aside class="st-blog-tags" aria-label="Filter by tag">
    <h2 class="st-blog-tags__title">Topics</h2>
    <ul class="st-blog-tags__list">
      <li class="st-blog-tags__item">
        {% if current_tags == empty %}
          <span class="st-blog-tags__link st-blog-tags__link--active" aria-current="true">All</span>
        {% else %}
          <a class="st-blog-tags__link" href="{{ blog.url }}">All</a>
        {% endif %}
      </li>
      {% for tag in blog.all_tags %}
        <li class="st-blog-tags__item">
          {% if current_tags contains tag %}
            <span class="st-blog-tags__link st-blog-tags__link--active" aria-current="true">{{ tag }}</span>
          {% else %}
            <a class="st-blog-tags__link" href="{{ blog.url }}/tagged/{{ tag | handleize }}">{{ tag }}</a>
          {% endif %}
        </li>
      {% endfor %}
    </ul>
  </aside>
{% endif %}

Article Comment List

Lists published comments on a blog article and shows a just-submitted comment ahead of the rest.

{% comment %}
  Article comment list. Paste into sections/main-article.liquid
  where you want published comments to appear. Shows a freshly
  posted comment first, then existing published comments, and
  avoids rendering it twice.
{% endcomment %}
{% if blog.comments_enabled? %}
  {% assign total = article.comments_count %}
  {% if comment and comment.created_at %}
    {% assign total = total | plus: 1 %}
    {% for existing in article.comments %}
      {% if existing.id == comment.id %}
        {% assign total = total | minus: 1 %}
        {% break %}
      {% endif %}
    {% endfor %}
  {% endif %}

  {% if total > 0 %}
    <section class="st-comments" id="comments">
      <h2 class="st-comments__title">{{ total }} comments</h2>
      <ul class="st-comments__list" role="list">
        {% if comment and comment.created_at %}
          <li class="st-comments__item st-comments__item--new">
            <div class="st-comments__body">{{ comment.content }}</div>
            <p class="st-comments__meta">{{ comment.author }}</p>
          </li>
        {% endif %}
        {% for published in article.comments %}
          <li class="st-comments__item">
            <div class="st-comments__body">{{ published.content }}</div>
            <p class="st-comments__meta">
              {{ published.author }} &middot; {{ published.created_at | time_tag: '%b %-d, %Y' }}
            </p>
          </li>
        {% endfor %}
      </ul>
    </section>
  {% endif %}
{% endif %}

Previous and next article buttons

Adds links to the previous and next articles within the same blog on an article page.

{% comment %}
  Previous / next article navigation within the same blog.
  Paste into your article template or main-article section.
{% endcomment %}
{% if blog.previous_article or blog.next_article %}
  <nav class="st-article-nav" aria-label="Article navigation">
    <ul class="st-article-nav__list" role="list">
      <li class="st-article-nav__item st-article-nav__item--prev">
        {% if blog.previous_article %}
          <a href="{{ blog.previous_article.url }}" rel="prev" class="st-article-nav__link">
            <span class="st-article-nav__direction">Previous post</span>
            <span class="st-article-nav__title">{{ blog.previous_article.title | escape }}</span>
          </a>
        {% endif %}
      </li>
      <li class="st-article-nav__item st-article-nav__item--next">
        {% if blog.next_article %}
          <a href="{{ blog.next_article.url }}" rel="next" class="st-article-nav__link">
            <span class="st-article-nav__direction">Next post</span>
            <span class="st-article-nav__title">{{ blog.next_article.title | escape }}</span>
          </a>
        {% endif %}
      </li>
    </ul>
  </nav>
{% endif %}

Article reading time

A “4 min read” label computed from the article's word count.

{% comment %}
  Reading time — article template; assumes roughly 200 words a minute.
{% endcomment %}
{% assign st_words = article.content | strip_html | split: ' ' | size %}
{% assign st_minutes = st_words | divided_by: 200 %}
{% if st_minutes < 1 %}{% assign st_minutes = 1 %}{% endif %}
<span class="st-reading">{{ st_minutes }} min read</span>

Navigation

Accessible Pagination

Renders a screen-reader-friendly numbered pagination nav with previous and next links inside a paginate block.

{% comment %}
  Accessible pagination controls. Place inside an existing paginate block
  in any template or section that loops over paginated content.
{% endcomment %}
{% if paginate.pages > 1 %}
  <nav class="st-pagination" role="navigation" aria-label="Pagination">
    <ul class="st-pagination__list">
      {% if paginate.previous %}
        <li class="st-pagination__item">
          <a class="st-pagination__link" href="{{ paginate.previous.url }}" rel="prev">
            <span aria-hidden="true">&larr;</span>
            <span class="st-visually-hidden">Previous page</span>
          </a>
        </li>
      {% endif %}
      {% for part in paginate.parts %}
        <li class="st-pagination__item">
          {% if part.is_link %}
            <a class="st-pagination__link" href="{{ part.url }}" aria-label="Page {{ part.title }}">{{ part.title }}</a>
          {% elsif part.title == paginate.current_page %}
            <span class="st-pagination__link st-pagination__link--current" aria-current="page">{{ part.title }}</span>
          {% else %}
            <span class="st-pagination__gap" aria-hidden="true">{{ part.title }}</span>
          {% endif %}
        </li>
      {% endfor %}
      {% if paginate.next %}
        <li class="st-pagination__item">
          <a class="st-pagination__link" href="{{ paginate.next.url }}" rel="next">
            <span class="st-visually-hidden">Next page</span>
            <span aria-hidden="true">&rarr;</span>
          </a>
        </li>
      {% endif %}
    </ul>
  </nav>
{% endif %}

Nested navigation menu

Renders a menu up to three levels deep from a linklist, marking the current page for accessibility.

{% comment %}
  Nested navigation: renders a linklist as a menu up to three levels deep.
  Paste into a header snippet or section. Change 'main-menu' to your menu handle.
{% endcomment %}
{% assign st_menu = linklists['main-menu'] %}
<nav class="st-nav" aria-label="Main">
  <ul class="st-nav__level st-nav__level--1" role="list">
    {% for parent in st_menu.links %}
      <li class="st-nav__item">
        <a href="{{ parent.url }}" class="st-nav__link"{% if parent.active %} aria-current="page"{% endif %}>{{ parent.title }}</a>
        {% if parent.links != blank %}
          <ul class="st-nav__level st-nav__level--2" role="list">
            {% for child in parent.links %}
              <li class="st-nav__item">
                <a href="{{ child.url }}" class="st-nav__link"{% if child.active %} aria-current="page"{% endif %}>{{ child.title }}</a>
                {% if child.links != blank %}
                  <ul class="st-nav__level st-nav__level--3" role="list">
                    {% for grandchild in child.links %}
                      <li class="st-nav__item">
                        <a href="{{ grandchild.url }}" class="st-nav__link"{% if grandchild.active %} aria-current="page"{% endif %}>{{ grandchild.title }}</a>
                      </li>
                    {% endfor %}
                  </ul>
                {% endif %}
              </li>
            {% endfor %}
          </ul>
        {% endif %}
      </li>
    {% endfor %}
  </ul>
</nav>

Simple pagination controls

Wraps a list in paginate tags and renders previous and next navigation links.

{% comment %}
  Simple pagination controls.
  Paste where you loop over a paginated collection, blog, or search results.
  Adjust per_page to change how many items appear before paging.
{% endcomment %}
{%- assign per_page = 8 -%}
{%- paginate collection.products by per_page -%}
  <ul class="st-list">
    {%- for product in collection.products -%}
      <li class="st-list__item">
        <a href="{{ product.url }}">{{ product.title }}</a>
      </li>
    {%- endfor -%}
  </ul>

  {%- if paginate.pages > 1 -%}
    <nav class="st-pager" role="navigation" aria-label="Pagination">
      {%- if paginate.previous -%}
        <a class="st-pager__link st-pager__link--prev" href="{{ paginate.previous.url }}">Newer</a>
      {%- endif -%}
      {%- if paginate.next -%}
        <a class="st-pager__link st-pager__link--next" href="{{ paginate.next.url }}">Older</a>
      {%- endif -%}
    </nav>
  {%- endif -%}
{%- endpaginate -%}

Social media icon navigation

Outputs an accessible list of social profile links from your theme settings.

{% comment %}
  Social media navigation.
  Paste into a header or footer section.
  Reads the social URLs from your theme settings (Online Store > Themes >
  Customize > Theme settings > Social media). Empty fields are skipped.
{% endcomment %}
<nav class="st-social" aria-label="Social media">
  <ul class="st-social__list">
    {%- assign networks = 'Facebook,Instagram,Pinterest,YouTube' | split: ',' -%}
    {%- assign urls = settings.social_facebook_link | append: '|' | append: settings.social_instagram_link | append: '|' | append: settings.social_pinterest_link | append: '|' | append: settings.social_youtube_link | split: '|' -%}
    {%- for name in networks -%}
      {%- assign url = urls[forloop.index0] -%}
      {%- if url != blank -%}
        <li class="st-social__item">
          <a class="st-social__link" href="{{ url }}" target="_blank" rel="noopener" aria-label="{{ name }}">
            {{ name }}
          </a>
        </li>
      {%- endif -%}
    {%- endfor -%}
  </ul>
</nav>

Sections

Announcement Bar

A toggleable announcement bar section with an optional link that can be limited to the homepage.

{% comment %}
  Announcement bar section. Save as sections/st-announcement-bar.liquid
  and add it to your theme via the theme editor.
{% endcomment %}
{% if section.settings.show_bar %}
  {% assign is_home = false %}
  {% if template.name == 'index' %}{% assign is_home = true %}{% endif %}
  {% if section.settings.home_only == false or is_home %}
    {% if section.settings.bar_link != blank %}
      <a class="st-announcement st-announcement--link" href="{{ section.settings.bar_link }}">
        {{ section.settings.bar_text | escape }}
      </a>
    {% else %}
      <div class="st-announcement">
        {{ section.settings.bar_text | escape }}
      </div>
    {% endif %}
  {% endif %}
{% endif %}

{% schema %}
{
  "name": "Announcement bar",
  "settings": [
    { "type": "checkbox", "id": "show_bar", "label": "Show announcement", "default": true },
    { "type": "checkbox", "id": "home_only", "label": "Homepage only", "default": true },
    { "type": "text", "id": "bar_text", "label": "Text", "default": "Free shipping on orders over $50" },
    { "type": "url", "id": "bar_link", "label": "Link (optional)" }
  ],
  "presets": [{ "name": "Announcement bar" }]
}
{% endschema %}

Call To Action

A promotional section with a heading, supporting text, and a configurable button.

{% comment %}
  Call-to-action section. Save as sections/st-call-to-action.liquid
  and add it through the theme editor.
{% endcomment %}
<div class="st-cta">
  {% if section.settings.heading != blank %}
    <h2 class="st-cta__heading">{{ section.settings.heading | escape }}</h2>
  {% endif %}
  {% if section.settings.body != blank %}
    <p class="st-cta__body">{{ section.settings.body | escape }}</p>
  {% endif %}
  {% if section.settings.button_link != blank and section.settings.button_label != blank %}
    <a class="st-cta__button" href="{{ section.settings.button_link }}">
      {{ section.settings.button_label | escape }}
    </a>
  {% endif %}
</div>

{% schema %}
{
  "name": "Call to action",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading", "default": "Ready to get started?" },
    { "type": "text", "id": "body", "label": "Text", "default": "Discover our latest arrivals." },
    { "type": "text", "id": "button_label", "label": "Button label", "default": "Shop now" },
    { "type": "url", "id": "button_link", "label": "Button link" }
  ],
  "presets": [{ "name": "Call to action" }]
}
{% endschema %}

Homepage Testimonials

A blocks-based testimonials section rendering each quote with an optional linked author citation.

{% comment %}
  Homepage testimonials. Save as sections/testimonials.liquid.
  Each block is one quote with an author and optional link. Add,
  remove, and reorder quotes from the theme editor.
{% endcomment %}
<div class="st-testimonials">
  {% if section.settings.heading != blank %}
    <h2 class="st-testimonials__title">{{ section.settings.heading | escape }}</h2>
  {% endif %}
  <div class="st-testimonials__list">
    {% for block in section.blocks %}
      {% if block.settings.quote != blank %}
        <figure class="st-testimonials__item" {{ block.shopify_attributes }}>
          <blockquote class="st-testimonials__quote">{{ block.settings.quote }}</blockquote>
          {% if block.settings.author != blank %}
            <figcaption class="st-testimonials__author">
              {% if block.settings.link != blank %}
                <cite><a href="{{ block.settings.link }}">{{ block.settings.author | escape }}</a></cite>
              {% else %}
                <cite>{{ block.settings.author | escape }}</cite>
              {% endif %}
            </figcaption>
          {% endif %}
        </figure>
      {% endif %}
    {% endfor %}
  </div>
</div>
{% schema %}
{
  "name": "Testimonials",
  "max_blocks": 8,
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading", "default": "What customers say" }
  ],
  "blocks": [
    {
      "type": "quote",
      "name": "Quote",
      "settings": [
        { "type": "richtext", "id": "quote", "label": "Quote", "default": "<p>Add a short customer quote here.</p>" },
        { "type": "text", "id": "author", "label": "Author" },
        { "type": "url", "id": "link", "label": "Author link" }
      ]
    }
  ],
  "presets": [{
    "name": "Testimonials",
    "blocks": [{ "type": "quote" }, { "type": "quote" }, { "type": "quote" }]
  }]
}
{% endschema %}

Logo list section

Displays a row of merchant-managed brand logos, each optionally linked, with placeholders when an image is missing.

{% comment %}
  Logo list: a grid of brand/partner logos managed as section blocks.
  Save as sections/st-logo-list.liquid and add it to any JSON template.
{% endcomment %}
<div class="st-logo-list">
  {% if section.settings.heading != blank %}
    <h2 class="st-logo-list__heading">{{ section.settings.heading | escape }}</h2>
  {% endif %}
  <ul class="st-logo-list__items" role="list">
    {% for block in section.blocks %}
      <li class="st-logo-list__item" style="width: {{ section.settings.logo_width }}px" {{ block.shopify_attributes }}>
        {%- capture logo_markup -%}
          {% if block.settings.image %}
            {{ block.settings.image | image_url: width: 320 | image_tag: alt: block.settings.image.alt, loading: 'lazy', class: 'st-logo-list__image' }}
          {% else %}
            {{ 'image' | placeholder_svg_tag: 'st-logo-list__image st-logo-list__image--placeholder' }}
          {% endif %}
        {%- endcapture -%}
        {% if block.settings.link != blank %}
          <a href="{{ block.settings.link }}" class="st-logo-list__link">{{ logo_markup }}</a>
        {% else %}
          {{ logo_markup }}
        {% endif %}
      </li>
    {% endfor %}
  </ul>
</div>

{% schema %}
{
  "name": "Logo list",
  "settings": [
    { "type": "text", "id": "heading", "label": "Heading", "default": "Trusted by" },
    { "type": "range", "id": "logo_width", "label": "Logo width", "min": 80, "max": 240, "step": 20, "unit": "px", "default": 160 }
  ],
  "blocks": [
    { "type": "logo", "name": "Logo", "limit": 12, "settings": [
      { "type": "image_picker", "id": "image", "label": "Logo image" },
      { "type": "url", "id": "link", "label": "Link (optional)" }
    ] }
  ],
  "presets": [ { "name": "Logo list", "blocks": [ { "type": "logo" }, { "type": "logo" }, { "type": "logo" }, { "type": "logo" } ] } ]
}
{% endschema %}

Global

Customer Login Form

Renders the customer login form with error handling, an optional guest checkout button, and a toggleable password recovery form.

{% comment %}
  Customer login. Paste into templates/customers/login.liquid
  (or sections/main-login.liquid). Includes the sign-in form,
  an optional guest-checkout button, and a password recovery
  form revealed by the recover link.
{% endcomment %}
<div class="st-login">
  {% form 'customer_login' %}
    {{ form.errors | default_errors }}
    <label for="st-login-email">Email</label>
    <input type="email" name="customer[email]" id="st-login-email" autocomplete="email" required>

    <label for="st-login-pass">Password</label>
    <input type="password" name="customer[password]" id="st-login-pass" autocomplete="current-password">

    <button type="submit">Sign in</button>
    <a href="#st-recover" onclick="document.getElementById('st-recover').hidden=false;return false;">Forgot your password?</a>
    {{ 'Create account' | customer_register_link }}
  {% endform %}

  {% if shop.checkout.guest_login %}
    {% form 'guest_login' %}
      <button type="submit">Continue as guest</button>
    {% endform %}
  {% endif %}

  <div class="st-login__recover" id="st-recover" hidden>
    {% form 'recover_customer_password' %}
      {% if form.posted_successfully? %}
        <p class="st-login__note">We have sent you a password reset email.</p>
      {% endif %}
      {{ form.errors | default_errors }}
      <label for="st-recover-email">Email</label>
      <input type="email" name="email" id="st-recover-email" autocomplete="email" required>
      <button type="submit">Reset password</button>
    {% endform %}
  </div>
</div>

Open Graph meta tags

Outputs Open Graph and Twitter Card meta tags tailored to the current page type for rich social sharing.

{% comment %}
  Open Graph / Twitter Card tags. Paste inside the <head> of theme.liquid,
  ideally after the title and description meta tags.
{% endcomment %}
<meta property="og:site_name" content="{{ shop.name }}">
<meta property="og:url" content="{{ canonical_url }}">
<meta property="og:title" content="{{ page_title | default: shop.name | escape }}">
<meta property="og:description" content="{{ page_description | default: shop.description | strip_html | escape }}">

{%- case template.name -%}
  {%- when 'product' -%}
    <meta property="og:type" content="product">
    {%- if product.featured_media.preview_image -%}
      <meta property="og:image" content="https:{{ product.featured_media.preview_image | image_url: width: 1200 }}">
    {%- endif -%}
    <meta property="product:price:amount" content="{{ product.price | money_without_currency | strip_html }}">
    <meta property="product:price:currency" content="{{ cart.currency.iso_code }}">
  {%- when 'article' -%}
    <meta property="og:type" content="article">
    {%- if article.image -%}
      <meta property="og:image" content="https:{{ article.image | image_url: width: 1200 }}">
    {%- endif -%}
  {%- else -%}
    <meta property="og:type" content="website">
{%- endcase -%}

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ page_title | default: shop.name | escape }}">
<meta name="twitter:description" content="{{ page_description | default: shop.description | strip_html | escape }}">

404 page content

A theme-agnostic 404 body with a message, a search form, and a link back to the homepage.

{% comment %}
  404 (page not found) content. Paste into templates/404.liquid,
  or a section rendered by the 404 JSON template.
{% endcomment %}
<div class="st-404">
  <h1 class="st-404__title">Page not found</h1>
  <p class="st-404__text">
    The page you were looking for doesn't exist. It may have been moved or removed.
  </p>

  <form action="{{ routes.search_url }}" method="get" role="search" class="st-404__search">
    <label for="st-404-search" class="st-404__label">Search the store</label>
    <input type="search"
           id="st-404-search"
           name="q"
           value="{{ search.terms | escape }}"
           placeholder="Search products..."
           class="st-404__input">
    <button type="submit" class="st-404__button">Search</button>
  </form>

  <p class="st-404__links">
    <a href="{{ routes.root_url }}" class="st-404__home">Return to the homepage</a>
  </p>
</div>

Password page content

Shows the store's password message alongside an optional email capture form for the password page.

{% comment %}
  Password page content: the store message plus an optional email signup.
  Paste into a section rendered by templates/password.json (main area).
{% endcomment %}
<div class="st-password-content">
  {% if shop.password_message != blank %}
    <div class="st-password-content__message rte">{{ shop.password_message }}</div>
  {% endif %}

  {% if section.settings.show_signup %}
    <div class="st-password-content__signup">
      {% if section.settings.signup_heading != blank %}
        <h2 class="st-password-content__heading">{{ section.settings.signup_heading | escape }}</h2>
      {% endif %}

      {% form 'customer', class: 'st-password-content__form' %}
        {% if form.posted_successfully? %}
          <p class="st-password-content__success">Thanks — we'll let you know when we launch.</p>
        {% else %}
          <input type="hidden" name="contact[tags]" value="prospect, password page">
          <label for="st-password-email" class="st-password-content__label">Email</label>
          <input type="email"
                 id="st-password-email"
                 name="contact[email]"
                 placeholder="[email protected]"
                 required
                 class="st-password-content__input">
          <button type="submit" class="st-password-content__button">Notify me</button>
        {% endif %}
      {% endform %}
    </div>
  {% endif %}
</div>

Storefront password form

The unlock form for a password-protected store, with an error message and an admin link.

{% comment %}
  Storefront password form: unlocks a password-protected store.
  Paste into the password page layout (password.liquid or its section).
{% endcomment %}
<div class="st-password-form">
  {% form 'storefront_password', class: 'st-password-form__form' %}
    {% if form.errors %}
      <p class="st-password-form__error" role="alert">
        {{ form.errors.messages['form'] | default: 'Incorrect password, please try again.' }}
      </p>
    {% endif %}

    <label for="st-store-password" class="st-password-form__label">Enter store password</label>
    <input type="password"
           id="st-store-password"
           name="password"
           autocomplete="off"
           class="st-password-form__input">
    <button type="submit" class="st-password-form__button">Enter</button>
  {% endform %}

  <p class="st-password-form__admin">
    Are you the store owner?
    <a href="/admin" class="st-password-form__admin-link">Log in here</a>
  </p>
</div>

Accepted payment icons

Renders SVG icons for every payment method the store accepts, typically for the footer.

{% comment %}
  Accepted payment icons: SVGs for each enabled payment method.
  Paste into the footer section or snippet.
{% endcomment %}
{% unless shop.enabled_payment_types == empty %}
  <div class="st-payments">
    <span class="st-payments__label st-visually-hidden">Accepted payment methods</span>
    <ul class="st-payments__list" role="list">
      {% for payment_type in shop.enabled_payment_types %}
        <li class="st-payments__item">
          {{ payment_type | payment_type_svg_tag: class: 'st-payments__icon' }}
        </li>
      {% endfor %}
    </ul>
  </div>
{% endunless %}

<style>
  .st-payments__list { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; padding: 0; }
  .st-payments__icon { width: 2.5rem; height: auto; }
  .st-visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
</style>

Signed-in customer greeting

Greets a signed-in customer by name and links their account; shows a sign-in link otherwise.

{% comment %}
  Customer greeting — header, or anywhere global.
{% endcomment %}
{% if customer %}
  <p class="st-greeting">
    Welcome back, {{ customer.first_name | default: 'friend' }} &mdash;
    you have {{ customer.orders_count }}
    {{ customer.orders_count | pluralize: 'order', 'orders' }} in
    <a href="{{ routes.account_url }}">your account</a>.
  </p>
{% else %}
  <a class="st-greeting" href="{{ routes.account_login_url }}">Sign in</a>
{% endif %}

Some snippets re-implement patterns popularised by Shopify's public Liquid examples, rewritten from scratch for this library; the rest are our own. Test in a duplicate theme before shipping to a live store.

Made with care by Astral Commerce