Sorting UI and Wiring to Shopify
Build a sorting interface that connects to Shopify's collection sorting, handling URL parameters and dynamic updates.
Sorting helps customers find products that match their priorities. Shopify provides built-in sorting options that you can expose through your theme’s UI.
Shopify’s Sort Options
Collections support these sort options:
| Value | Display Name |
|---|---|
manual | Featured |
best-selling | Best Selling |
title-ascending | Alphabetically, A-Z |
title-descending | Alphabetically, Z-A |
price-ascending | Price, Low to High |
price-descending | Price, High to Low |
created-ascending | Date, Old to New |
created-descending | Date, New to Old |
How Sorting Works
Sorting uses URL parameters. When a customer selects a sort option, the page reloads with:
/collections/all?sort_by=price-ascendingAccess the current sort in Liquid:
{{ collection.sort_by }} {# "price-ascending" #}{{ collection.default_sort_by }} {# "manual" (or collection default) #}Basic Sort Dropdown
{# snippets/collection-sorting.liquid #}
<div class="collection-sorting"> <label for="sort-by" class="collection-sorting__label"> Sort by </label>
<select id="sort-by" class="collection-sorting__select" onchange="window.location.href = this.value" > {%- assign sort_options = 'manual,best-selling,title-ascending,title-descending,price-ascending,price-descending,created-descending' | split: ',' -%}
{%- for option in sort_options -%} {%- assign option_url = collection.url | append: '?sort_by=' | append: option -%} <option value="{{ option_url }}" {% if collection.sort_by == option %}selected{% endif %} > {%- case option -%} {%- when 'manual' -%} Featured {%- when 'best-selling' -%} Best Selling {%- when 'title-ascending' -%} Alphabetically, A-Z {%- when 'title-descending' -%} Alphabetically, Z-A {%- when 'price-ascending' -%} Price, Low to High {%- when 'price-descending' -%} Price, High to Low {%- when 'created-descending' -%} Newest {%- endcase -%} </option> {%- endfor -%} </select></div>Preserving Filter Parameters
When sorting, preserve any active filters:
{%- liquid assign current_url = collection.url
# Check if filters are active if collection.filters.size > 0 assign filter_params = '' for filter in collection.filters for value in filter.active_values assign filter_params = filter_params | append: '&' | append: filter.param_name | append: '=' | append: value.param_name | url_encode endfor endfor endif-%}
<select onchange="window.location.href = this.value"> {%- for option in sort_options -%} {%- assign option_url = current_url | append: '?sort_by=' | append: option | append: filter_params -%} <option value="{{ option_url }}" {% if collection.sort_by == option %}selected{% endif %}> ... </option> {%- endfor -%}</select>Using collection.sort_options
Shopify provides a sort_options object:
<select id="sort-by" onchange="window.location.href = this.value"> {%- for option in collection.sort_options -%} <option value="{{ option.url }}" {% if option.value == collection.sort_by %}selected{% endif %} > {{ option.name }} </option> {%- endfor -%}</select>This automatically includes the correct URLs with any existing parameters.
Styled Sort Component
{# snippets/collection-sorting.liquid #}
<div class="collection-sorting"> <div class="collection-sorting__wrapper"> <label for="sort-by" class="collection-sorting__label"> Sort by: </label>
<div class="collection-sorting__select-wrapper"> <select id="sort-by" class="collection-sorting__select" data-sort-select> {%- for option in collection.sort_options -%} <option value="{{ option.url }}" {% if option.value == collection.sort_by %}selected{% endif %} > {{ option.name }} </option> {%- endfor -%} </select>
<span class="collection-sorting__icon"> {% render 'icon-chevron-down' %} </span> </div> </div></div>.collection-sorting { display: flex; align-items: center; gap: var(--spacing-md);}
.collection-sorting__wrapper { display: flex; align-items: center; gap: var(--spacing-sm);}
.collection-sorting__label { font-size: 0.875rem; color: var(--color-text-light); white-space: nowrap;}
.collection-sorting__select-wrapper { position: relative;}
.collection-sorting__select { appearance: none; padding: var(--spacing-sm) var(--spacing-xl) var(--spacing-sm) var(--spacing-sm); font-size: 0.875rem; font-weight: 500; background: var(--color-background); border: 1px solid var(--color-border); border-radius: var(--border-radius); cursor: pointer; min-width: 180px;}
.collection-sorting__select:focus { outline: 2px solid var(--color-focus); outline-offset: 2px;}
.collection-sorting__icon { position: absolute; right: var(--spacing-sm); top: 50%; transform: translateY(-50%); pointer-events: none; color: var(--color-text-light);}
.collection-sorting__icon svg { width: 16px; height: 16px;}Button-Style Sort Selector
An alternative UI pattern:
<div class="sort-buttons" role="radiogroup" aria-label="Sort products"> {%- for option in collection.sort_options -%} <a href="{{ option.url }}" class="sort-button {% if option.value == collection.sort_by %}sort-button--active{% endif %}" role="radio" aria-checked="{{ option.value == collection.sort_by }}" > {{ option.name }} </a> {%- endfor -%}</div>.sort-buttons { display: flex; flex-wrap: wrap; gap: var(--spacing-xs);}
.sort-button { padding: var(--spacing-xs) var(--spacing-sm); font-size: 0.8125rem; color: var(--color-text-light); text-decoration: none; border: 1px solid var(--color-border); border-radius: var(--border-radius); transition: all 0.2s;}
.sort-button:hover { border-color: var(--color-text); color: var(--color-text);}
.sort-button--active { background: var(--color-text); color: var(--color-background); border-color: var(--color-text);}AJAX Sorting (No Page Reload)
For a smoother experience, update the grid without a full page reload:
class CollectionSorting extends HTMLElement { connectedCallback() { this.select = this.querySelector('[data-sort-select]'); this.select.addEventListener('change', this.onSort.bind(this)); }
async onSort(event) { const url = event.target.value;
// Update URL without reload history.pushState({}, '', url);
// Show loading state this.showLoading();
try { // Fetch the new page content const response = await fetch(url); const html = await response.text();
// Parse and update the product grid const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html');
const newGrid = doc.querySelector('.product-grid'); const currentGrid = document.querySelector('.product-grid');
if (newGrid && currentGrid) { currentGrid.innerHTML = newGrid.innerHTML; }
// Update pagination const newPagination = doc.querySelector('.pagination'); const currentPagination = document.querySelector('.pagination');
if (currentPagination) { if (newPagination) { currentPagination.innerHTML = newPagination.innerHTML; } else { currentPagination.remove(); } } } catch (error) { console.error('Sort failed:', error); // Fall back to page reload window.location.href = url; } finally { this.hideLoading(); } }
showLoading() { document.querySelector('.product-grid')?.classList.add('is-loading'); }
hideLoading() { document.querySelector('.product-grid')?.classList.remove('is-loading'); }}
customElements.define('collection-sorting', CollectionSorting);.product-grid.is-loading { opacity: 0.5; pointer-events: none;}Collection Toolbar
Combine sorting with product count and view options:
{# snippets/collection-toolbar.liquid #}
<div class="collection-toolbar"> <div class="collection-toolbar__left"> <p class="collection-toolbar__count"> {{ collection.products_count }} {{ collection.products_count | pluralize: 'product', 'products' }} </p> </div>
<div class="collection-toolbar__right"> {# View toggle (grid/list) #} {%- if section.settings.show_view_toggle -%} <div class="collection-toolbar__view"> <button class="view-toggle view-toggle--grid is-active" aria-label="Grid view" data-view="grid" > {% render 'icon-grid' %} </button> <button class="view-toggle view-toggle--list" aria-label="List view" data-view="list" > {% render 'icon-list' %} </button> </div> {%- endif -%}
{# Sort dropdown #} {%- if section.settings.show_sort -%} {% render 'collection-sorting' %} {%- endif -%} </div></div>.collection-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--spacing-md); padding: var(--spacing-md) 0; border-bottom: 1px solid var(--color-border); margin-bottom: var(--spacing-lg);}
.collection-toolbar__left { display: flex; align-items: center; gap: var(--spacing-md);}
.collection-toolbar__right { display: flex; align-items: center; gap: var(--spacing-lg);}
.collection-toolbar__count { font-size: 0.875rem; color: var(--color-text-light); margin: 0;}
.collection-toolbar__view { display: flex; gap: var(--spacing-xs);}
.view-toggle { display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; background: none; border: 1px solid var(--color-border); border-radius: var(--border-radius); cursor: pointer; color: var(--color-text-light); transition: all 0.2s;}
.view-toggle:hover,.view-toggle.is-active { color: var(--color-text); border-color: var(--color-text);}
.view-toggle svg { width: 18px; height: 18px;}
/* Mobile: Stack toolbar */@media (max-width: 767px) { .collection-toolbar { flex-direction: column; align-items: stretch; }
.collection-toolbar__right { justify-content: space-between; }}Storing Sort Preference
Remember the user’s sort preference:
class CollectionSorting extends HTMLElement { connectedCallback() { this.select = this.querySelector('[data-sort-select]');
// Restore saved preference this.restorePreference();
this.select.addEventListener('change', (e) => { this.savePreference(e.target.value); window.location.href = e.target.value; }); }
savePreference(url) { const sortBy = new URL(url, window.location.origin).searchParams.get('sort_by'); if (sortBy) { localStorage.setItem('preferred_sort', sortBy); } }
restorePreference() { // Only apply if no sort is specified in URL if (window.location.search.includes('sort_by')) return;
const preferred = localStorage.getItem('preferred_sort'); if (preferred) { const option = Array.from(this.select.options).find((opt) => opt.value.includes(`sort_by=${preferred}`) ); if (option && !option.selected) { window.location.href = option.value; } } }}Practice Exercise
Build a collection toolbar that:
- Shows product count
- Has a sort dropdown with all options
- Preserves filters when sorting
- Shows a loading state during sort
- Works without JavaScript (fallback)
Test by:
- Sorting different ways
- Checking URL updates correctly
- Combining with filters (if available)
Key Takeaways
- Use
collection.sort_optionsfor automatic URL handling - Current sort:
collection.sort_by - Preserve existing parameters when changing sort
- Style the select for better UX
- Consider AJAX updates for smoother experience
- Combine in a toolbar with count and view options
- Store preferences for returning visitors
- Always work without JS as a fallback
What’s Next?
With sorting in place, the next lesson covers Filtering Concepts: Native Filters and Tags for more advanced product filtering.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...