Setelah pembeli masuk lewat menu di 36 Navigasi dan Menu, mereka biasanya mendarat di halaman koleksi berisi ratusan produk. Filtering itu yang bikin ratusan jadi sepuluh yang relevan.

Ada dua cara filter produk di theme Shopify:

  1. Storefront filtering, cara yang disarankan. Merchant bikin grup filter dari data produk yang udah ada (ketersediaan, harga, opsi varian, metafield) lewat app Search & Discovery, dan filternya berlaku di koleksi dan hasil pencarian.
  2. Tag filtering, cara lama pakai tag produk. Cuma buat koleksi, dan merchant harus bikin sistem tag manual.

Bab ini bahas keduanya, dengan porsi terbesar buat storefront filtering.

Storefront Filtering

Filter bisa dibikin dari data produk dan varian:

  1. Availability (ketersediaan)
  2. Category (kategori produk)
  3. Price (harga)
  4. Product tags
  5. Product type
  6. Vendor
  7. Variant options (warna, ukuran)
  8. Metafields

Logikanya: antar filter pakai AND, antar nilai dalam satu filter pakai OR. Jadi “warna merah dan ukuran L”, tapi “warna merah atau biru”. Merchant bisa bikin maksimal 25 filter.

Dua batasan penting: koleksi dengan lebih dari 5.000 produk gak nampilin filter, dan hasil pencarian dengan lebih dari 1.000 produk juga gak. Dan sebelum bisa dipakai, filternya harus dibikin dulu di admin.

URL Parameter Filter

Filter yang aktif tercermin di URL halaman. Ini bagus: URL bisa dibagiin, tombol back jalan, dan theme gak perlu nyimpen state sendiri. Strukturnya:

filter.filter_scope.attribute=value
filter.filter_scope.attribute.attribute_scope=value

Komponennya:

  1. filter, namespace tetap.
  2. filter_scope, p buat level produk, v buat level varian.
  3. attribute, atribut yang difilter.
  4. attribute_scope (opsional), buat option, price, dan metafield.
  5. value, nilai filternya.

Contoh filter tipe produk shoes dan opsi varian Color red di koleksi all:

/collections/all?filter.p.product_type=shoes&filter.v.option.color=red

Banyak nilai buat satu filter bisa dua cara, hasilnya sama:

filter.v.option.color=red,blue
filter.v.option.color=red&filter.v.option.color=blue

1. Filter Level Produk

  1. Category: t.category, nilai satu ID kategori atau daftar dipisah __ (aa-1 atau aa-1__aa-2).
  2. Product tags: tag, satu tag atau daftar dipisah koma (new atau new,trending).
  3. Product type: product_type (shoes atau shoes,belts).
  4. Vendor: vendor (vendor1 atau vendor1,vendor2).
  5. Metafield: m + namespace + key sebagai attribute_scope. Metafield custom.made_in jadi m.custom.made_in, nilainya canada atau canada,usa. Tipe metafield yang didukung: single_line_text_field, list.single_line_text_field, metaobject_reference, list.metaobject_reference, number_integer, number_decimal, boolean.
// Product tag
filter.p.tag=new,trending
// Product type
filter.p.product_type=shoes
// Product vendor
filter.p.vendor=vendor1
// Product metafield
filter.p.m.custom.made_in=canada

Catatan buat metafield: daftar dipisah koma itu daftar nilai metafield individual, bukan satu nilai metafield yang isinya ada komanya.

2. Filter Level Varian

  1. Availability: availability, nilai 0 (habis), 1 (tersedia), atau 0,1 (keduanya).
  2. Variant option: option + nama opsi sebagai attribute_scope, misalnya option.color, nilai red atau red,blue.
  3. Price: price + kondisi sebagai attribute_scope: lte (kurang dari atau sama dengan) atau gte (lebih dari atau sama dengan). Nilainya angka dalam mata uang default toko (5 atau 20.40).
  4. Standard product attribute: t + shopify + key, misalnya t.shopify.fabric. Ini metafield standar list.metaobject_reference yang nyambung ke opsi produk. Nilainya GID metaobject atau GID nilai taksonomi (gid://shopify/Metaobject/1).
  5. Metafield varian: m.custom.fabric, sama kaya metafield produk tapi di level varian.
// Variant availability
filter.v.availability=1
// Variant price
filter.v.price.lte=5
// Variant option
filter.v.option.color=red
// Standard product attribute
filter.v.t.shopify.fabric=gid://shopify/Metaobject/1
// Variant metafields
filter.v.m.custom.fabric=leather

Efek samping yang berguna: waktu filter varian aktif, product.featured_media dan product.url berubah buat nunjuk ke varian pertama yang cocok. Jadi kalau pembeli filter warna merah, kartu produk otomatis nampilin gambar varian merah dan link-nya deep link ke varian merah. Deep link varian dibahas di 39 Produk Variants dan High-variant Products.

Bikin Tampilan Filter

Yang kamu butuhin: object filter (lewat collection.filters atau search.filters), yang propertinya udah dibahas di 17 Objects Lainnya > Object Filtering. Ringkasnya:

  1. filter.label, nama grup filter.
  2. filter.type: list, price_range, atau boolean.
  3. filter.param_name, nama parameter URL.
  4. filter.values, semua nilai (buat list), tiap nilai punya label, value, count, active, param_name, url_to_add, url_to_remove, display.
  5. filter.active_values, nilai yang lagi aktif.
  6. filter.url_to_remove, URL tanpa filter ini.
  7. filter.min_value, filter.max_value, filter.range_max buat price_range.
  8. filter.true_value, filter.false_value buat boolean.

Tampilan filter ditaruh di template collection (atau section di dalamnya), dan di template search. Implementasi dasarnya pakai form dengan tombol submit: waktu disubmit, halaman reload dengan filter di URL. JavaScript bisa ditambahin buat submit otomatis dan update grid tanpa reload lewat 51 Ajax API dan Section Rendering API, tapi dasarnya harus jalan tanpa JavaScript. Implementasi lengkapnya bisa dilihat di Dawn.

1. Form Filter Koleksi

Dua komponen: daftar grup filter dan nilainya, dan daftar filter aktif. Ini contoh lengkapnya, kita bedah bagian per bagian setelahnya:

<form>
  {%- for filter in collection.filters -%}
    <details>
      <summary>
        <div>
          <span>{{ filter.label }}</span>
          {%- if filter.active_values.size > 0 -%}
            <span>({{ filter.active_values.size }})</span>
          {%- endif -%}
        </div>
      </summary>
      <div>
        <div>
          <p>{{ filter.active_values.size }} selected</p>
          {%- if filter.active_values.size > 0 -%}
            <p><a href="{{ filter.url_to_remove }}">Reset</a></p>
          {%- endif -%}
        </div>
        {%- case filter.type -%}
          {%- when 'boolean' -%}
            <ul>
                <li>
                  <label for="Filter-{{ filter.param_name }}-{{ filter.true_value.value }}">
                  <input type="checkbox"
                    name="{{ filter.param_name }}"
                    value="{{ filter.true_value.value }}"
                    id="Filter-{{ filter.param_name }}"
                    {% if filter.true_value.active -%}checked{%- endif %}
                    {% if filter.true_value.count == 0 and filter.true_value.active == false -%}disabled{%- endif -%}
                   />{{ filter.true_value.label }}</label>
                </li>
                <li>
                  <label for="Filter-{{ filter.param_name }}-{{ filter.false_value.value }}">
                  <input type="checkbox"
                    name="{{ filter.param_name }}"
                    value="{{ filter.false_value.value }}"
                    id="Filter-{{ filter.param_name }}"
                    {% if filter.false_value.active -%}checked{%- endif %}
                    {% if filter.false_value.count == 0 and filter.false_value.active == false -%}disabled{%- endif %}
                   />{{ filter.false_value.label }}</label>
                </li>
            </ul>
            <div>
              <input type="submit" value="Apply" />
            </div>
          {%- when 'list' -%}
            <ul>
              {%- for filter_value in filter.values -%}
                <li>
                  <label for="Filter-{{ filter.param_name }}-{{ forloop.index }}">
                  <input type="checkbox"
                    name="{{ filter_value.param_name }}"
                    value="{{ filter_value.value }}"
                    id="Filter-{{ filter.param_name }}-{{ forloop.index }}"
                    {% if filter_value.active -%}checked{%- endif %}
                    {% if filter_value.count == 0 and filter_value.active == false -%}disabled{%- endif %}
                   />
                    <span>
                      {%- case filter_value.display.type -%}
                        {%- when 'colors' -%}
                          {% liquid
                            assign size_limit = filter_value.display.value.size | at_most: 4
                            assign rotation = '0deg'
                            if size_limit == 2
                              assign rotation = '45deg'
                            endif
                            assign angle_increment = 360 | divided_by: size_limit
                            assign angle = 0
                          %}
                          {%- capture conic_gradient -%}
                            {%- for color in filter_value.display.value limit: size_limit -%}
                              {{ color }} {{ angle }}deg{%- assign angle = angle | plus: angle_increment %} {{ angle }}deg{%- unless forloop.last %}, {%- endunless -%}
                            {%- endfor -%}
                          {%- endcapture -%}
                          <span style="width: 25px; height: 25px; border-radius: 50%; display: flex; justify-content: center; align-items: center; background: conic-gradient({{ conic_gradient }}); transform: rotateZ({{ rotation }});"></span>
                        {%- when 'image' -%}
                          {{
                            filter_value.display.value
                            | image_url: width: 25
                            | image_tag: alt: filter_value.display.value.alt
                          }}
                        {%- else -%}
                          <span class="visual-display__child"></span>
                      {%- endcase -%}
                    </span>
                    {{ filter_value.label }}
                  </label>
                </li>
              {%- endfor -%}
            </ul>
            <div>
              <input type="submit" value="Apply" />
            </div>
          {%- when 'price_range' -%}
            <div class="filter-group-display__price-range">
              <div class="filter-group-display__price-range-from">
                <span>{{ cart.currency.symbol }}</span>
                <input name="{{ filter.min_value.param_name }}"
                  id="Filter-{{ filter.min_value.param_name }}"
                  {% if filter.min_value.value -%}
                    value="{{ filter.min_value.value | money_without_currency | replace: ',', '' }}"
                  {%- endif %}
                  type="number"
                  placeholder="0"
                  min="0"
                  max="{{ filter.range_max | money_without_currency | replace: ',', '' }}"
                 />
                <label for="Filter-{{ filter.min_value.param_name }}">From</label>
              </div>
              <div class="filter-group-display__price-range-to">
                <span>{{ cart.currency.symbol }}</span>
                <input name="{{ filter.max_value.param_name }}"
                  id="Filter-{{ filter.max_value.param_name }}"
                  {% if filter.max_value.value -%}
                    value="{{ filter.max_value.value | money_without_currency | replace: ',', '' }}"
                  {%- endif %}
                  type="number"
                  placeholder="{{ filter.range_max | money_without_currency | replace: ',', '' }}"
                  min="0"
                  max="{{ filter.range_max | money_without_currency | replace: ',', '' }}"
                 />
                <label for="Filter-{{ filter.max_value.param_name }}">To</label>
              </div>
            </div>
            <div class="filter-group-display__submit">
              <input type="submit" value="Apply" />
            </div>
        {%- endcase -%}
      </div>
    </details>
  {%- endfor -%}
  <div>
    <p><a href="{{ collection.url }}?sort_by={{ collection.sort_by }}">Clear all</a></p>
    {%- for filter in collection.filters -%}
      {%- if filter.type == "price_range" -%}
        {%- if filter.min_value.value != nil or filter.max_value.value != nil -%}
          <p>
            <a href="{{ filter.url_to_remove }}">
              {%- assign min_value = filter.min_value.value | default: 0 -%}
              {%- assign max_value = filter.max_value.value | default: filter.range_max -%}
              {{ min_value | money }} - {{ max_value | money }} X
            </a>
          </p>
        {%- endif -%}
      {%- else -%}
        {%- for filter_value in filter.active_values -%}
          <p>
            <a href="{{ filter_value.url_to_remove }}">
              {{ filter.label }}: {{ filter_value.label }} X
            </a>
          </p>
        {%- endfor -%}
      {%- endif -%}
    {%- endfor -%}
  </div>
</form>

2. Bedah: Grup Filter

Tiap grup dibungkus <details> (bisa dilipat, aksesibel tanpa JavaScript, pola yang sama dengan dropdown di 36 Navigasi dan Menu > Dropdown yang Aksesibel). <summary>-nya nampilin filter.label dan jumlah nilai aktif dalam kurung. Di dalamnya ada link Reset ke filter.url_to_remove kalau ada yang aktif.

3. Bedah: Tiga Tipe Filter

Ini inti form-nya, case filter.type:

  1. boolean: dua checkbox, satu buat true_value, satu buat false_value. Nama input pakai filter.param_name.
  2. list: loop filter.values, tiap nilai jadi checkbox dengan name="{{ filter_value.param_name }}" dan value="{{ filter_value.value }}". Karena nama dan nilainya diambil dari object, form ini otomatis ngasilin URL parameter yang bener (filter.v.option.color=red) tanpa kamu nyusun string sendiri.
  3. price_range: dua input angka, min_value dan max_value, dengan max dari filter.range_max. Nilainya dilewatin money_without_currency | replace: ',', '' biar jadi angka polos tanpa pemisah ribuan.

Tiga hal yang berlaku di semua checkbox:

  1. checked kalau active, biar state dari URL tercermin.
  2. disabled kalau count == 0 dan gak aktif, biar pembeli gak milih nilai yang hasilnya kosong. Ini prinsip “hindari jalan buntu” di bawah.
  3. id unik buat pasangan label/input.

❌ nilai dengan nol hasil tetap bisa dipilih, pembeli mendarat di halaman kosong

<input type="checkbox" name="{{ filter_value.param_name }}" value="{{ filter_value.value }}">

✅ dinonaktifin

<input type="checkbox" name="{{ filter_value.param_name }}" value="{{ filter_value.value }}"
  {% if filter_value.count == 0 and filter_value.active == false %}disabled{% endif %}>

4. Bedah: Swatch Warna dan Gambar

filter_value.display (object filter_value_display dari 17 Objects Lainnya > 3. filter_value_display) ngasih tampilan visual buat nilai filter, yang merchant atur di Search & Discovery:

  1. colors: array warna. Contoh di atas ngerakit conic-gradient biar satu lingkaran bisa nampilin sampai 4 warna (dibagi rata, diputar 45° kalau cuma dua biar kelihatan diagonal). Ini cara nampilin swatch “merah/putih” tanpa gambar.
  2. image: object gambar, dirender pakai image_url + image_tag ukuran 25px.
  3. Selain itu: placeholder kosong.

5. Bedah: Filter Aktif

Bagian bawah form nampilin chip filter aktif yang bisa dihapus satu-satu, plus Clear all:

  1. Clear all nunjuk ke collection.url dengan sort_by dipertahankan, biar ngehapus filter gak ngereset urutan.
  2. Price range: tampil kalau min_value.value atau max_value.value ada, dengan default 0 dan range_max, dihapus lewat filter.url_to_remove.
  3. Lainnya: loop filter.active_values, tiap chip nunjuk ke filter_value.url_to_remove dan nampilin label grup plus label nilai.

6. Form Filter Hasil Pencarian

Hampir identik, dengan tiga perbedaan:

  1. Sumbernya search.filters, bukan collection.filters.
  2. Ada input tersembunyi q berisi search.terms biar kata kuncinya ikut waktu form disubmit.
  3. Clear all nunjuk ke routes.search_url dengan sort_by dan q.
<form>
  <input type="hidden" name="q" value="{{ search.terms }}" />
  {%- for filter in search.filters -%}
    ...
  {%- endfor -%}
  <div>
    <p><a href="{{ routes.search_url }}?sort_by={{ search.sort_by }}&q={{ search.terms }}">Clear all</a></p>
    ...
  </div>
</form>

Catatan: kalau filter diterapin di hasil pencarian, semua hasil non-produk (artikel, halaman) dibuang. Detail pencariannya di 38 Search dan Predictive Search.

Sorting

Filter dan sort selalu berdampingan. Urutannya lewat parameter sort_by di URL, dan collection.sort_options (juga search.sort_options) ngasih daftar opsi dengan name dan value, dibahas di 14 Objects Produk dan Koleksi dan 17 Objects Lainnya > sort_option:

<select name="sort_by" id="SortBy">
  {% for option in collection.sort_options %}
    <option value="{{ option.value }}"{% if option.value == collection.sort_by %} selected{% endif %}>
      {{ option.name }}
    </option>
  {% endfor %}
</select>

Taruh di form yang sama dengan filter biar satu submit ngirim dua-duanya, dan selalu pertahankan sort_by di link Clear all.

Pedoman UX Filtering

Filtering itu langsung ngaruh ke konversi, jadi Shopify ngasih pedoman yang jelas:

1. Antarmuka yang Tepat per Tipe

  1. list: checkbox, biar jelas bisa milih banyak sekaligus.
  2. price_range: slider dua pegangan, tapi tetap sediain input angka “From” dan “To” buat aksesibilitas, dengan label (boleh disembunyiin secara visual).

2. Pilihan Layout

Dua layout standar di layar besar, dan sebaiknya dua-duanya disediain di balik theme setting:

  1. Toolbar horizontal di atas grid: kompak, gabungin filter dan sort, cocok buat toko kecil dengan kurang dari lima filter.
  2. Sidebar vertikal di kiri grid: cocok buat toko dengan lebih dari lima filter.

3. Hierarki Visual yang Jelas

Nama grup filter harus menonjol dari nilainya, dan antar grup harus ada pemisah yang jelas.

4. Hindari Jalan Buntu

Pembeli gak boleh berakhir dengan nol hasil. Selalu tampilin jumlah hasil (count) di tiap nilai, dan nonaktifin nilai yang hasilnya nol.

5. Tampilin Filter yang Aktif

Kalau pembeli mendarat di halaman yang udah ada filternya (dari link yang dibagiin), mereka harus lihat filter apa yang aktif. Kalau nambahin filter, harus ada umpan balik. Dan harus gampang ngehapus satu filter atau semua.

Biasanya label nilai udah cukup jelas (“Sepatu”, “Tersedia”). Tapi kadang gak, misalnya ada filter “Lebar” dan “Tinggi” yang nilainya sama-sama angka. Jadi sediain theme setting buat nyertain nama grup di chip filter aktif (“Lebar: 30”).

6. Kelola Grup Filter

Di sidebar vertikal, lipat grup setelah lima pertama, biar gampang dipindai tanpa scroll panjang.

7. Kelola Nilai Filter

Satu grup bisa punya sampai 100 nilai. Potong jadi 10 dengan tombol “Show more”. Dan kalau bisa, tampilin 10 yang paling relevan, bukan 10 pertama.

8. Mobile

Pindahin antarmuka filter ke drawer atau modal. Dua cara nampilinnya:

  1. Satu tombol “Filter” yang buka drawer.
  2. Daftar grup filter yang scroll horizontal, tiap grup buka modal filternya sendiri, plus tombol “Filter” sebagai cadangan.

9. Progressive Enhancement

Bangun sebanyak mungkin tanpa JavaScript: link atau form dengan tombol submit manual. JavaScript ditambahin di atasnya buat update grid secara live. Kalau JavaScript-nya gagal dimuat, filter tetap jalan. Prinsip ini dibahas lagi di 61 Performance JavaScript dan INP.

Tag Filtering (Cara Lama)

Shopify sendiri bilang: pertimbangkan storefront filtering daripada ini. Tapi theme lama masih banyak yang makainya.

1. Cara Kerja

Filter tag diterapin dengan nambahin /[tag-handle] ke URL koleksi, handle-nya versi handleize dari tag:

{shop}.myshopify.com/collections/frontpage/new

Banyak tag digabung pakai +, logikanya AND:

{shop}.myshopify.com/collections/frontpage/new+sale

Kalau gak ada produk yang punya kedua tag, pembeli lihat halaman kosong, jalan buntu yang persis dilarang pedoman UX di atas.

2. Redirect Tag yang Gak Dipakai

Kalau tag di URL gak dipakai produk manapun di toko, Shopify redirect ke URL tanpa tag itu. Promo summer-sale selesai dan tag-nya dihapus dari semua produk? Link lama /collections/frontpage/summer-sale otomatis ke /collections/frontpage. Kalau ada banyak tag, cuma yang gak berlaku yang dibuang.

3. Implementasi

Loop collection.all_tags, cek ada di current_tags atau gak, dan output link tambah/hapus pakai filter link_to_add_tag dan link_to_remove_tag dari 19 Filters HTML, URL, Media, Color, dan Font > 3. Filter Tag Filtering:

{% if collection.all_tags.size > 0 %}
  <ul class="tag-filters">
    {% for tag in collection.all_tags %}
      {% if current_tags contains tag %}
        <li class="tag-filters__item active">{{ tag | link_to_remove_tag: tag }}</li>
      {% else %}
        <li class="tag-filters__item">{{ tag | link_to_add_tag: tag }}</li>
      {% endif %}
    {% endfor %}
  </ul>
{% endif %}

Kenapa storefront filtering lebih baik: merchant gak perlu ngerawat sistem tag manual, filternya bisa dari harga/ketersediaan/opsi varian yang gak mungkin jadi tag, ada count buat nyegah jalan buntu, dan berlaku juga di hasil pencarian.

Ringkasan

  1. Dua cara filter: storefront filtering (disarankan, dari Search & Discovery, koleksi dan pencarian) dan tag filtering (lama, koleksi aja).
  2. Sumber filter: availability, category, price, tag, type, vendor, variant option, metafield. AND antar filter, OR antar nilai. Maksimal 25 filter, gak tampil kalau koleksi > 5.000 produk atau pencarian > 1.000.
  3. URL parameter: filter.p.* level produk (tag, product_type, vendor, t.category, m.ns.key), filter.v.* level varian (availability, option.nama, price.lte/gte, t.shopify.key, m.ns.key). Banyak nilai pakai koma.
  4. Filter varian aktif ngubah product.featured_media dan product.url ke varian pertama yang cocok.
  5. Form filter: loop collection.filters/search.filters, case filter.type buat boolean (true/false value), list (checkbox dari filter.values dengan param_name/value dari object), price_range (input min/max dengan range_max).
  6. Selalu: checked kalau active, disabled kalau count == 0, swatch dari filter_value.display (colors → conic-gradient, image → image_tag).
  7. Filter aktif sebagai chip dengan url_to_remove, plus Clear all yang mempertahankan sort_by (dan q di pencarian).
  8. Pencarian: input tersembunyi q, dan filter aktif ngebuang hasil non-produk.
  9. UX: checkbox buat list, slider + input angka buat harga, layout toolbar/sidebar sebagai setting, hierarki jelas, tanpa jalan buntu, filter aktif kelihatan dan bisa dihapus, lipat grup setelah 5, potong nilai ke 10, drawer di mobile, progressive enhancement.
  10. Tag filtering: /collections/x/tag1+tag2 (AND), redirect otomatis buat tag yang gak dipakai, implementasi lewat all_tags, current_tags, link_to_add_tag/link_to_remove_tag.

Bab selanjutnya kita bahas pencarian: form search dan parameter URL-nya, object search, cara nampilin hasil campuran (produk, artikel, halaman), dan predictive search yang nampilin saran waktu pembeli lagi ngetik lewat Ajax API.