Skip to main content

Support storefront filtering

Storefront filtering allows merchants to easily create filters for filtering collection and search results pages. Filters are based on existing product data, like availability, price, variant options, and more.

In this tutorial, you'll learn how to support storefront filtering in your theme.


To implement storefront filtering, you'll use the following:


Anchor to Implementing storefront filteringImplementing storefront filtering

To support storefront filtering, you need to implement a filter display for customers to interact with.

Applied filters are reflected through URL parameters, so you should also familiarize yourself with the structure of filter URL parameters.

Note

Before filters can be applied, they need to be created in the Shopify admin.


Note

Collections that contain over 5,000 products don't display filters.

The following sections outline a basic collection and search results filter implementation. Each implementation uses a form to host the filter inputs with "submit" buttons to apply the associated filters. However, you can use JavaScript to automatically submit the form based on input changes. When the form is submitted, the page is refreshed with the filters applied.

For a more in-depth solution, refer to Dawn's implementation.

Tip

Refer to Storefront filtering UX guidelines for more information on UX best practices when building a filter display.

Anchor to Collection filter displayCollection filter display

The collection filter display should be included in the collection template, or a section that's included as part of the collection template.

The following example implementation contains two main components:

  • A list of filter groups and values.

  • A list of active filters, if there are any.

    Each of these components are output through the filters attribute of the collection object, and the associated filter objects.

Example collection filter display

<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', borderRadius: '50%', display: 'flex', justifyContent: 'center', alignItems: '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 className="visual-display__child"></span>
{%- endcase -%}
</span>
{{ filter_value.label }}
</label>
</li>
{%- endfor -%}
</ul>

<div>
<input type="submit" value="Apply" />
</div>
{%- when 'price_range' -%}
<div className="filter-group-display__price-range">
<div className="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 className="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 className="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>

Anchor to Search results filter displaySearch results filter display

Note

Search results that exceed 1,000 products don't display filters.

The search results filter display should be included in the search template, or in a section that's included as part of the search template.

The following example implementation contains two main components:

  • A list of filter groups and values.

  • A list of active filters, if there are any.

    Each of these components are output through the filters attribute of the search object, and the associated filter objects.

    If you apply filters on the search results page, then all non-product results are filtered out.

Example search results filter display

<form>
<input type="hidden" name="q" value="{{ search.terms }}" />

{%- for filter in search.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', borderRadius: '50%', display: 'flex', justifyContent: 'center', alignItems: '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 className="visual-display__child"></span>
{%- endcase -%}
</span>
{{ filter_value.label }}
</label>
</li>
{%- endfor -%}
</ul>

<div>
<input type="submit" value="Apply" />
</div>
{%- when 'price_range' -%}
<div className="filter-group-display__price-range">
<div className="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 className="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 className="filter-group-display__submit">
<input type="submit" value="Apply" />
</div>
{%- endcase -%}
</div>
</details>
{%- endfor -%}

<div>
<p><a href="{{ routes.search_url }}?sort_by={{ search.sort_by }}&q={{ search.terms }}">Clear all</a></p>
{%- for filter in search.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>

Was this page helpful?