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 -%}