How Liquid filters work

A Liquid filter takes the value on its left and transforms it. Filters are applied with the pipe character |. They chain left to right, each filter receiving the output of the previous one:

{%- comment -%}Single filter{%- endcomment -%}
{{ product.title | upcase }}
{%- comment -%}→ "CLASSIC WHITE TEE"{%- endcomment -%}

{%- comment -%}Chained filters — read left to right{%- endcomment -%}
{{ product.title | downcase | replace: ' ', '-' }}
{%- comment -%}→ "classic-white-tee"{%- endcomment -%}

{%- comment -%}Filter with arguments{%- endcomment -%}
{{ product.price | money_with_currency }}
{%- comment -%}→ "$24.99 USD"{%- endcomment -%}

The whitespace-stripping dashes ({%- and -%}) in Liquid tags prevent unwanted whitespace in the rendered HTML. Good practice to use them in production themes.

String filters

Used constantly in templates — product titles, descriptions, handles, and any text content.

{%- comment -%} upcase / downcase {%- endcomment -%}
{{ "hello world" | upcase }}         {%- comment -%}→ HELLO WORLD{%- endcomment -%}
{{ "HELLO WORLD" | downcase }}       {%- comment -%}→ hello world{%- endcomment -%}

{%- comment -%} capitalize — first letter upper, rest lower {%- endcomment -%}
{{ "blue medium" | capitalize }}     {%- comment -%}→ Blue medium{%- endcomment -%}

{%- comment -%} replace / remove {%- endcomment -%}
{{ "Hello World" | replace: "World", "Shopify" }}
{%- comment -%}→ Hello Shopify{%- endcomment -%}
{{ "Hello World" | remove: "World" }}
{%- comment -%}→ Hello {%- endcomment -%}

{%- comment -%} strip_html — remove HTML tags from rich text fields {%- endcomment -%}
{{ product.description | strip_html | truncate: 160 }}

{%- comment -%} truncate — cut with ellipsis (default "...") {%- endcomment -%}
{{ product.title | truncate: 40 }}
{{ product.title | truncate: 40, "…" }}  {%- comment -%}custom ellipsis{%- endcomment -%}

{%- comment -%} truncatewords {%- endcomment -%}
{{ product.description | strip_html | truncatewords: 20 }}

{%- comment -%} prepend / append {%- endcomment -%}
{{ product.handle | prepend: '/products/' }}
{%- comment -%}→ /products/classic-white-tee{%- endcomment -%}

{%- comment -%} split / join {%- endcomment -%}
{%- assign tags_array = product.tags | join: ", " -%}
{%- comment -%} join: turn array into string {%- endcomment -%}

{%- comment -%} newline_to_br — convert line breaks in plain text to <br> {%- endcomment -%}
{{ product.description | newline_to_br }}

{%- comment -%} strip / lstrip / rstrip — remove whitespace {%- endcomment -%}
{{ "  hello  " | strip }}            {%- comment -%}→ "hello"{%- endcomment -%}

Array filters

Essential for working with product variants, collections, tags, metafield lists, and any array output.

{%- comment -%} size — length of string or array {%- endcomment -%}
{{ product.variants | size }}        {%- comment -%}→ 4{%- endcomment -%}
{{ product.title | size }}           {%- comment -%}→ character count{%- endcomment -%}

{%- comment -%} first / last {%- endcomment -%}
{{ product.images | first }}
{{ collection.products | last }}

{%- comment -%} map — extract a property from each object in an array {%- endcomment -%}
{%- assign variant_titles = product.variants | map: 'title' -%}
{{ variant_titles | join: ', ' }}
{%- comment -%}→ "Small, Medium, Large, XL"{%- endcomment -%}

{%- comment -%} where — filter array by property value {%- endcomment -%}
{%- assign available_variants = product.variants | where: 'available', true -%}
{%- for variant in available_variants -%}
  <option value="{{ variant.id }}">{{ variant.title }}</option>
{%- endfor -%}

{%- comment -%} sort / sort_natural (case-insensitive) {%- endcomment -%}
{%- assign sorted_products = collection.products | sort: 'price' -%}
{%- assign sorted_tags = product.tags | sort_natural -%}

{%- comment -%} uniq — remove duplicates {%- endcomment -%}
{%- assign unique_types = collection.products | map: 'type' | uniq -%}

{%- comment -%} reverse {%- endcomment -%}
{%- assign reversed = product.images | reverse -%}

{%- comment -%} concat — merge two arrays {%- endcomment -%}
{%- assign all_products = collection_a.products | concat: collection_b.products -%}

{%- comment -%} compact — remove nil values from array {%- endcomment -%}
{%- assign clean_array = some_array | compact -%}

Math filters

{%- comment -%} plus / minus / times / divided_by / modulo {%- endcomment -%}
{{ 10 | plus: 3 }}      {%- comment -%}→ 13{%- endcomment -%}
{{ 10 | minus: 3 }}     {%- comment -%}→ 7{%- endcomment -%}
{{ 10 | times: 3 }}     {%- comment -%}→ 30{%- endcomment -%}
{{ 10 | divided_by: 3 }} {%- comment -%}→ 3 (integer division!){%- endcomment -%}
{{ 10.0 | divided_by: 3 }} {%- comment -%}→ 3.3333 (float division){%- endcomment -%}
{{ 10 | modulo: 3 }}    {%- comment -%}→ 1{%- endcomment -%}

{%- comment -%} round / ceil / floor {%- endcomment -%}
{{ 4.6 | round }}       {%- comment -%}→ 5{%- endcomment -%}
{{ 4.6 | ceil }}        {%- comment -%}→ 5{%- endcomment -%}
{{ 4.6 | floor }}       {%- comment -%}→ 4{%- endcomment -%}
{{ 4.567 | round: 2 }}  {%- comment -%}→ 4.57{%- endcomment -%}

{%- comment -%} abs {%- endcomment -%}
{{ -12 | abs }}         {%- comment -%}→ 12{%- endcomment -%}

{%- comment -%} Real example: calculate discount percentage {%- endcomment -%}
{%- assign discount = product.compare_at_price
  | minus: product.price
  | times: 100.0
  | divided_by: product.compare_at_price
  | round -%}
<span class="badge-discount">-{{ discount }}%</span>

Money filters

Prices in Shopify Liquid are integers in the store's subunit (cents for USD). Money filters handle the formatting, including currency symbols and locale rules.

{%- comment -%} money — uses store currency format from settings {%- endcomment -%}
{{ product.price | money }}
{%- comment -%}→ $24.99{%- endcomment -%}

{%- comment -%} money_with_currency {%- endcomment -%}
{{ product.price | money_with_currency }}
{%- comment -%}→ $24.99 USD{%- endcomment -%}

{%- comment -%} money_without_currency {%- endcomment -%}
{{ product.price | money_without_currency }}
{%- comment -%}→ 24.99{%- endcomment -%}

{%- comment -%} money_without_trailing_zeros — hides .00 cents {%- endcomment -%}
{{ product.price | money_without_trailing_zeros }}
{%- comment -%}→ $24 (if price is $24.00){%- endcomment -%}

{%- comment -%} Real pattern: sale price display {%- endcomment -%}
{%- if product.compare_at_price > product.price -%}
  <s class="price-compare">{{ product.compare_at_price | money }}</s>
  <span class="price-sale">{{ product.price | money }}</span>
{%- else -%}
  <span class="price">{{ product.price | money }}</span>
{%- endif -%}

URL filters

{%- comment -%} asset_url — CDN URL for files in the assets/ folder {%- endcomment -%}
{{ 'theme.css' | asset_url | stylesheet_tag }}
{{ 'main.js' | asset_url }}
{%- comment -%}→ https://cdn.shopify.com/s/files/1/0000/assets/main.js?v=123{%- endcomment -%}

{%- comment -%} stylesheet_tag / script_tag — generate full HTML tags {%- endcomment -%}
{{ 'section-hero.css' | asset_url | stylesheet_tag }}
{{ 'cart-drawer.js' | asset_url | script_tag }}

{%- comment -%} url_for_type / url_for_vendor {%- endcomment -%}
{{ product.type | url_for_type }}
{%- comment -%}→ /collections/types?q=Shirts{%- endcomment -%}
{{ product.vendor | url_for_vendor }}

{%- comment -%} link_to — generate a full anchor tag {%- endcomment -%}
{{ product.title | link_to: product.url }}
{%- comment -%}→ <a href="/products/classic-white-tee">Classic White Tee</a>{%- endcomment -%}

{%- comment -%} url_escape / url_encode — safe URL parameters {%- endcomment -%}
{{ "search term" | url_encode }}
{%- comment -%}→ search+term{%- endcomment -%}

Date filters

{%- comment -%} date — format date with strftime directives {%- endcomment -%}
{{ article.published_at | date: "%B %d, %Y" }}
{%- comment -%}→ January 15, 2026{%- endcomment -%}

{{ article.published_at | date: "%Y-%m-%d" }}
{%- comment -%}→ 2026-01-15  (good for datetime= attribute){%- endcomment -%}

{{ article.published_at | date: "%b %d" }}
{%- comment -%}→ Jan 15{%- endcomment -%}

{%- comment -%} Common directives:
  %Y = 4-digit year
  %y = 2-digit year
  %m = month number (01-12)
  %B = full month name
  %b = abbreviated month
  %d = day of month (01-31)
  %e = day without leading zero
  %H = hour (00-23)
  %I = hour (01-12)
  %M = minute (00-59)
  %p = AM/PM
{%- endcomment -%}

{%- comment -%} Real pattern: accessible datetime markup {%- endcomment -%}
<time datetime="{{ article.published_at | date: "%Y-%m-%dT%H:%M:%SZ" }}">
  {{ article.published_at | date: "%B %e, %Y" }}
</time>

Image filters — image_url and image_tag

The image filters are some of the most important in Shopify theme development. Shopify's CDN can resize, crop, and convert images on the fly.

{%- comment -%} image_url with width — always specify width {%- endcomment -%}
{{ product.featured_image | image_url: width: 800 }}
{%- comment -%}→ CDN URL for 800px wide, WebP when supported{%- endcomment -%}

{%- comment -%} image_url with crop {%- endcomment -%}
{{ product.featured_image | image_url: width: 400, height: 400, crop: 'center' }}
{%- comment -%} crop: 'top', 'bottom', 'left', 'right', 'center' {%- endcomment -%}

{%- comment -%} image_tag — generates the full <img> element {%- endcomment -%}
{{ product.featured_image
  | image_url: width: 800
  | image_tag:
    loading: 'lazy',
    width: product.featured_image.width,
    height: product.featured_image.height,
    alt: product.featured_image.alt
}}

{%- comment -%} image_tag with srcset (widths) {%- endcomment -%}
{{ product.featured_image
  | image_url: width: 800
  | image_tag:
    loading: 'lazy',
    sizes: '(min-width: 768px) 50vw, 100vw',
    widths: '400, 600, 800, 1000',
    alt: product.featured_image.alt
}}
{%- comment -%}
  Generates: srcset="...400w, ...600w, ...800w, ...1000w"
  The browser picks the right size automatically.
{%- endcomment -%}

{%- comment -%} Adding CSS class and ID via image_tag {%- endcomment -%}
{{ product.featured_image
  | image_url: width: 800
  | image_tag:
    class: 'product-image',
    id: 'main-product-image',
    loading: 'lazy',
    alt: product.featured_image.alt
}}

Metafield filters

When you expose metafields in your Liquid templates, the metafield_tag filter renders the metafield with the appropriate HTML for its type:

{%- comment -%}
  metafield_tag renders the metafield as the right HTML element for its type:
  - file_reference (image) → <img>
  - rating → <span> with structured markup
  - rich_text → rendered HTML
{%- endcomment -%}
{{ product.metafields.custom.spec_sheet | metafield_tag }}
{{ product.metafields.reviews.rating | metafield_tag }}

{%- comment -%} Access raw value without rendering {%- endcomment -%}
{{ product.metafields.custom.subtitle.value }}

{%- comment -%} List metafield (returns array) {%- endcomment -%}
{%- for item in product.metafields.custom.features.value -%}
  <li>{{ item }}</li>
{%- endfor -%}

Color filters

Shopify's color filters let you manipulate color values defined in theme settings — useful for generating hover states, tints, and accessible contrast pairs dynamically:

{%- comment -%}
  settings.color_button is a color setting (e.g. #7c6fff)
{%- endcomment -%}

{%- comment -%} color_lighten / color_darken (0-100) {%- endcomment -%}
{{ settings.color_button | color_lighten: 10 }}
{{ settings.color_button | color_darken: 15 }}

{%- comment -%} color_saturate / color_desaturate (0-100) {%- endcomment -%}
{{ settings.color_button | color_saturate: 20 }}

{%- comment -%} color_mix — blend two colors {%- endcomment -%}
{{ settings.color_button | color_mix: settings.color_background, 50 }}

{%- comment -%} color_contrast — ratio between two colors (accessibility) {%- endcomment -%}
{%- assign contrast = settings.color_button | color_contrast: settings.color_button_text -%}
{%- if contrast < 4.5 -%}
  {%- comment -%}warn: low contrast, WCAG AA fail for body text{%- endcomment -%}
{%- endif -%}

{%- comment -%} color_to_rgb / color_to_hsl — convert format {%- endcomment -%}
{{ settings.color_button | color_to_rgb }}
{%- comment -%}→ rgb(124, 111, 255){%- endcomment -%}

{%- comment -%} Real pattern: CSS custom properties from theme settings {%- endcomment -%}
<style>
  :root {
    --color-button: {{ settings.color_button }};
    --color-button-hover: {{ settings.color_button | color_darken: 10 }};
    --color-button-rgb: {{ settings.color_button | color_to_rgb | remove: 'rgb(' | remove: ')' }};
  }
</style>

The color_contrast filter is underused. Use it to validate accessible contrast ratios between a theme setting color pair before outputting CSS. If the merchant picks a low-contrast combination in the theme editor, you can fall back to a legible default rather than silently produce an inaccessible design.

The translate filter (t)

Every hardcoded string in a Shopify theme should go through the t filter so it's translatable. The string is a dot-separated key into the locale files:

{%- comment -%} Basic usage {%- endcomment -%}
{{ 'products.product.add_to_cart' | t }}
{%- comment -%}→ "Add to cart" (from locales/en.default.json){%- endcomment -%}

{%- comment -%} With variable interpolation {%- endcomment -%}
{{ 'cart.items_count' | t: count: cart.item_count }}
{%- comment -%}→ "3 items" (locale key: "cart.items_count": "{{ count }} items"){%- endcomment -%}

{%- comment -%} Pluralization {%- endcomment -%}
{{ 'products.product.inventory_count' | t: count: variant.inventory_quantity }}
{%- comment -%}
  Locale:
  "inventory_count": {
    "one": "Only {{ count }} left",
    "other": "{{ count }} left in stock"
  }
{%- endcomment -%}