← back to Designer Wallcoverings
pending-approval/toolbar-consolidation-assets/snippets__collection-toolbar.liquid
331 lines
{%- comment -%}
Collection Toolbar — Horizontal sort + grid density slider + search
Replaces Shopify's native sorting/filtering UI with a unified toolbar.
Features:
- Sort dropdown (Newest, Color, Style, SKU, Title, Price)
- Grid density slider (3–12 columns)
- localStorage persistence
- Responsive mobile stacking
Revision: 2026-06-22
{%- endcomment -%}
<div class="collection-toolbar">
<style>
.collection-toolbar {
display: flex;
gap: 16px;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
padding: 20px 0;
border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.1));
margin-bottom: 24px;
}
.toolbar-left {
display: flex;
gap: 14px;
align-items: center;
flex-wrap: nowrap;
min-width: 0;
}
.toolbar-label {
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
font-weight: 600;
color: var(--text-muted, rgba(0,0,0,0.6));
}
#collectionSort {
background: transparent;
border: 1px solid var(--border-color, rgba(0,0,0,0.12));
padding: 6px 10px;
font-family: inherit;
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
font-weight: 500;
color: var(--text-color, inherit);
cursor: pointer;
transition: all 0.2s ease;
min-width: 140px;
max-width: 100%;
overflow: visible;
}
#collectionSort:hover {
border-color: var(--accent-color, rgba(0,0,0,0.3));
}
#collectionSort:focus {
outline: 2px solid var(--accent-color, #000);
outline-offset: 2px;
}
.density-control {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 0;
}
.density-control input[type="range"] {
flex: 0 0 120px;
-webkit-appearance: none;
appearance: none;
width: 120px;
height: 4px;
background: var(--border-color, rgba(0,0,0,0.15));
outline: none;
cursor: pointer;
border-radius: 2px;
}
.density-control input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 13px;
height: 13px;
background: var(--accent-color, #000);
border-radius: 50%;
cursor: pointer;
transition: transform 0.2s ease;
}
.density-control input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.density-control input[type="range"]::-moz-range-thumb {
width: 13px;
height: 13px;
background: var(--accent-color, #000);
border-radius: 50%;
border: 0;
cursor: pointer;
transition: transform 0.2s ease;
}
.density-control input[type="range"]::-moz-range-thumb:hover {
transform: scale(1.2);
}
.density-display {
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
font-weight: 600;
color: var(--text-muted, rgba(0,0,0,0.6));
min-width: 60px;
text-align: right;
}
.toolbar-search {
flex: 0 1 200px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--border-color, rgba(0,0,0,0.1));
padding: 4px 0;
}
.toolbar-search input {
flex: 1;
background: transparent;
border: 0;
padding: 4px 0;
font-family: inherit;
font-size: 12px;
letter-spacing: 0.04em;
color: var(--text-color, inherit);
outline: none;
}
.toolbar-search input::placeholder {
color: var(--text-muted, rgba(0,0,0,0.4));
}
.toolbar-search svg {
width: 14px;
height: 14px;
stroke: var(--text-muted, rgba(0,0,0,0.4));
fill: none;
stroke-width: 1.5;
flex-shrink: 0;
}
/* Mobile responsive — hide density slider on mobile */
@media (max-width: 768px) {
.collection-toolbar {
gap: 12px;
}
.toolbar-left {
flex: 1 1 100%;
order: 1;
flex-wrap: wrap;
}
.toolbar-search {
flex: 1 1 100%;
order: 2;
margin-top: 8px;
}
.density-control {
display: none !important;
visibility: hidden !important;
width: 0 !important;
height: 0 !important;
margin: 0 !important;
padding: 0 !important;
border: 0 !important;
}
}
/* Extra-small devices — ensure slider is completely hidden */
@media (max-width: 480px) {
.density-control {
display: none !important;
visibility: hidden !important;
pointer-events: none !important;
}
}
</style>
<div class="toolbar-left">
<label for="collectionSort" class="toolbar-label">Sort</label>
<select id="collectionSort" aria-label="Sort products">
<option value="newest">Newest</option>
<option value="color">Color</option>
<option value="style">Style</option>
<option value="sku">SKU A→Z</option>
<option value="title">Title A→Z</option>
<option value="price-asc">Price ↑</option>
<option value="price-desc">Price ↓</option>
</select>
<div class="density-control">
<span class="toolbar-label">Grid</span>
<input type="range" id="densitySlider" min="3" max="12" value="4" aria-label="Grid density">
<div class="density-display"><span id="densityLabel">4</span> col</div>
</div>
</div>
{% if section.settings.show_search %}
<div class="toolbar-search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
<input type="text" id="collectionSearch" placeholder="Search products…" aria-label="Search products">
</div>
{% endif %}
</div>
<script>
(function() {
const sortSelect = document.getElementById('collectionSort');
const densitySlider = document.getElementById('densitySlider');
const densityLabel = document.getElementById('densityLabel');
const searchInput = document.getElementById('collectionSearch');
const STORAGE_KEY_SORT = 'dw_collection_sort';
const STORAGE_KEY_DENSITY = 'dw_collection_density';
const STORAGE_KEY_SEARCH = 'dw_collection_search';
// Hard guard: if the canonical toolbar markup isn't on this page, bail out
// entirely so a missing optional control (e.g. search when disabled) can never
// throw "Cannot read properties of null". 2026-06-23 consolidation.
if (!sortSelect || !densitySlider || !densityLabel) return;
// Load saved state
try {
const savedSort = localStorage.getItem(STORAGE_KEY_SORT);
if (savedSort) sortSelect.value = savedSort;
const savedDensity = localStorage.getItem(STORAGE_KEY_DENSITY);
if (savedDensity) {
densitySlider.value = savedDensity;
densityLabel.textContent = savedDensity;
updateGridDensity(parseInt(savedDensity));
}
const savedSearch = localStorage.getItem(STORAGE_KEY_SEARCH);
if (savedSearch && searchInput) searchInput.value = savedSearch;
} catch (e) {}
// Sort change
sortSelect.addEventListener('change', function() {
try { localStorage.setItem(STORAGE_KEY_SORT, this.value); } catch (e) {}
applySort(this.value);
});
// Density slider
densitySlider.addEventListener('input', function() {
const cols = parseInt(this.value);
densityLabel.textContent = cols;
try { localStorage.setItem(STORAGE_KEY_DENSITY, cols); } catch (e) {}
updateGridDensity(cols);
});
// Search (optional — only present when section.settings.show_search is on)
if (searchInput) {
searchInput.addEventListener('input', debounce(function() {
try { localStorage.setItem(STORAGE_KEY_SEARCH, this.value); } catch (e) {}
applySearch(this.value);
}, 300));
}
function updateGridDensity(cols) {
const productGrid = document.querySelector('.collection-products') || document.querySelector('[data-collection-grid]') || document.querySelector('[role="list"]') || document.querySelector('[data-product-grid]');
if (productGrid) {
productGrid.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
productGrid.setAttribute('data-cols', cols);
}
}
function applySort(mode) {
const url = new URL(window.location);
if (mode === 'newest') {
url.searchParams.delete('sort_by');
} else {
// Map DW sort modes to Shopify's sort_by values
const sortMap = {
'title': 'title-ascending',
'price-asc': 'price-ascending',
'price-desc': 'price-descending'
};
url.searchParams.set('sort_by', sortMap[mode] || mode);
}
window.location.href = url.toString();
}
function applySearch(query) {
if (query.trim() === '') {
window.location.href = window.location.pathname;
} else {
const url = new URL(window.location);
url.searchParams.set('q', query);
window.location.href = url.toString();
}
}
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// Initialize grid on page load
window.addEventListener('load', function() {
updateGridDensity(parseInt(densitySlider.value));
});
})();
</script>