Description
- adding Filter by Variant Options to Store Page
- view demo – password: abc
- buy me a coffee

#1. Install Code
#1.1. Click Gear icon on Store Page

Next, click Advanced > Page Header Code Injection

#1.2. Add this code to right box
<!-- 07.26c10v4 Filter by Variant Options -->
<script>
window.StoreVariantFilterConfig = {
sectionSelector: '.product-list-container',
spinnerTime: 500,
format: 'dropdown',
logic: 'AND',
multiSelect: false,
showCount: true,
hideEmpty: true,
allLabel: 'All',
search: {
enable: false,
placeholder: 'Search products...',
width: '240px',
background: '#ffffff',
border: '1px solid #000000',
borderRadius: '4px',
textSize: '14px',
textColor: '#000000',
placeholderColor: '#888888'
},
style: {
gap: '15px',
rowGap: '15px',
marginBottom: '30px',
fontSize: '14px',
activeColor: 'transparent',
activeTextColor: '#000000',
inactiveColor: 'transparent',
inactiveTextColor: '#000000',
dropdown: {
minWidth: '150px',
background: '#ffffff',
border: '1px solid #e5e5e5',
borderRadius: '3px',
textSize: '14px',
textColor: '#333333',
panelBackground: '#ffffff',
hoverBackground: '#f5f5f5'
}
}
};
</script>
<script>
(function() {
const cfg = window.StoreVariantFilterConfig;
if (!cfg) return;
const st = cfg.style;
const dd = st.dropdown;
let allItems = [];
let filterGroups = [];
let searchTerm = '';
let spinnerTimer;
function injectCSS() {
if (document.getElementById('svf-styles')) return;
const style = document.createElement('style');
style.id = 'svf-styles';
style.innerHTML = `
.svf-bar { display: flex; flex-wrap: wrap; align-items: center; gap: ${st.gap}; row-gap: ${st.rowGap}; margin-bottom: ${st.marginBottom}; }
.svf-groups { display: flex; flex-wrap: wrap; align-items: center; gap: ${st.gap}; row-gap: ${st.rowGap}; }
.svf-dd { position: relative; display: inline-block; }
.svf-dd-head { display: inline-flex; align-items: center; justify-content: space-between; min-width: ${dd.minWidth}; padding: 10px 15px; background: ${dd.background}; border: ${dd.border}; border-radius: ${dd.borderRadius}; color: ${dd.textColor}; font-size: ${dd.textSize}; font-family: inherit; cursor: pointer; }
.svf-dd-list { list-style: none; margin: 5px 0 0; padding: 5px 0; position: absolute; top: 100%; left: 0; min-width: 100%; z-index: 50; background: ${dd.panelBackground}; border: ${dd.border}; border-radius: ${dd.borderRadius}; display: none; max-height: 250px; overflow: auto; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
.svf-dd-list li { padding: 4px 15px; cursor: pointer; font-size: ${dd.textSize}; color: ${dd.textColor}; white-space: nowrap; font-weight: 400; line-height: 1.2; }
.svf-dd-list li:hover { background: ${dd.hoverBackground}; }
.svf-search { margin-left: auto; box-sizing: border-box; width: ${cfg.search.width}; background: ${cfg.search.background}; border: ${cfg.search.border}; border-radius: ${cfg.search.borderRadius}; color: ${cfg.search.textColor}; font-size: ${cfg.search.textSize}; padding: 10px 15px; outline: none; font-family: inherit;}
.svf-search::placeholder { color: ${cfg.search.placeholderColor}; }
.svf-spinner { display: none; width: 16px; height: 16px; border: 2px solid rgba(0,0,0,0.1); border-top: 2px solid ${dd.textColor}; border-radius: 50%; animation: svf-spin 0.8s linear infinite; margin-left: 5px; flex-shrink: 0; align-self: center; }
@keyframes svf-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
`;
document.head.appendChild(style);
}
function fetchStoreData(url) {
return fetch(url + '?format=json').then(res => res.json());
}
function processData(data, domItems) {
const attributeMap = {};
const itemsMap = new Map();
data.items.forEach(item => {
const attributes = {};
if(item.variants && item.variants.length > 0) {
item.variants.forEach(variant => {
if(variant.attributes) {
Object.keys(variant.attributes).forEach(key => {
if(!attributeMap[key]) attributeMap[key] = new Set();
attributeMap[key].add(variant.attributes[key]);
if(!attributes[key]) attributes[key] = new Set();
attributes[key].add(variant.attributes[key]);
});
}
});
}
itemsMap.set(item.id, attributes);
});
Object.keys(attributeMap).forEach(key => {
filterGroups.push({
label: key,
options: Array.from(attributeMap[key]).sort(),
selected: new Set()
});
});
domItems.forEach(el => {
const pid = el.getAttribute('data-product-id');
if (pid && itemsMap.has(pid)) {
allItems.push({ el: el, data: itemsMap.get(pid), title: el.querySelector('.product-list-item-title').textContent.toLowerCase() });
}
});
}
function getCount(groupLabel, option) {
let count = 0;
allItems.forEach(item => {
if (item.data[groupLabel] && item.data[groupLabel].has(option)) count++;
});
return count;
}
function applyFilters() {
const spinner = document.querySelector('.svf-spinner');
if (spinner) spinner.style.display = 'block';
clearTimeout(spinnerTimer);
allItems.forEach(item => {
let matchSearch = true;
if (searchTerm) matchSearch = item.title.includes(searchTerm);
let matchFilter = true;
const activeGroups = filterGroups.filter(g => g.selected.size > 0);
if (activeGroups.length > 0) {
if (cfg.logic === 'AND') {
matchFilter = activeGroups.every(g => {
let hasAttr = false;
g.selected.forEach(val => {
if (item.data[g.label] && item.data[g.label].has(val)) hasAttr = true;
});
return hasAttr;
});
} else {
matchFilter = activeGroups.some(g => {
let hasAttr = false;
g.selected.forEach(val => {
if (item.data[g.label] && item.data[g.label].has(val)) hasAttr = true;
});
return hasAttr;
});
}
}
item.el.style.display = (matchSearch && matchFilter) ? '' : 'none';
});
document.querySelectorAll('.svf-dd-head').forEach(head => {
const index = head.getAttribute('data-index');
const g = filterGroups[index];
const labelEl = head.querySelector('.svf-dd-label');
if (g.selected.size === 0) labelEl.textContent = g.label;
else labelEl.textContent = Array.from(g.selected).join(', ');
});
document.querySelectorAll('.svf-dd-list li').forEach(li => {
const groupIdx = li.getAttribute('data-group');
const opt = li.getAttribute('data-opt');
const g = filterGroups[groupIdx];
if(opt === '__all__') li.style.fontWeight = g.selected.size === 0 ? '700' : '400';
else li.style.fontWeight = g.selected.has(opt) ? '700' : '400';
});
// Cập nhật lấy thời gian từ file cấu hình
spinnerTimer = setTimeout(() => {
if (spinner) spinner.style.display = 'none';
}, cfg.spinnerTime || 1000);
}
function buildUI(container) {
if(filterGroups.length === 0 && !cfg.search.enable) return;
const bar = document.createElement('div');
bar.className = 'svf-bar';
const groupsWrap = document.createElement('div');
groupsWrap.className = 'svf-groups';
filterGroups.forEach((g, idx) => {
const wrap = document.createElement('div');
wrap.className = 'svf-dd';
const head = document.createElement('button');
head.className = 'svf-dd-head';
head.setAttribute('data-index', idx);
head.innerHTML = `<span class="svf-dd-label">${g.label}</span><span style="margin-left:8px;">▾</span>`;
const ul = document.createElement('ul');
ul.className = 'svf-dd-list';
if (cfg.allLabel) {
const liAll = document.createElement('li');
liAll.textContent = cfg.allLabel;
liAll.setAttribute('data-group', idx);
liAll.setAttribute('data-opt', '__all__');
liAll.onclick = (e) => {
e.stopPropagation();
g.selected.clear();
ul.style.display = 'none';
applyFilters();
};
ul.appendChild(liAll);
}
g.options.forEach(opt => {
const count = getCount(g.label, opt);
if (cfg.hideEmpty && count === 0) return;
const li = document.createElement('li');
li.textContent = cfg.showCount ? `${opt} (${count})` : opt;
li.setAttribute('data-group', idx);
li.setAttribute('data-opt', opt);
li.onclick = (e) => {
e.stopPropagation();
if (cfg.multiSelect) {
if(g.selected.has(opt)) g.selected.delete(opt); else g.selected.add(opt);
} else {
g.selected.clear(); g.selected.add(opt);
ul.style.display = 'none';
}
applyFilters();
};
ul.appendChild(li);
});
head.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const isOpen = ul.style.display === 'block';
document.querySelectorAll('.svf-dd-list').forEach(l => l.style.display = 'none');
ul.style.display = isOpen ? 'none' : 'block';
};
wrap.appendChild(head);
wrap.appendChild(ul);
groupsWrap.appendChild(wrap);
});
const spinner = document.createElement('div');
spinner.className = 'svf-spinner';
groupsWrap.appendChild(spinner);
bar.appendChild(groupsWrap);
if (cfg.search.enable) {
const input = document.createElement('input');
input.className = 'svf-search';
input.placeholder = cfg.search.placeholder;
input.oninput = (e) => {
searchTerm = e.target.value.toLowerCase();
applyFilters();
};
bar.appendChild(input);
}
container.insertBefore(bar, container.firstChild);
document.addEventListener('click', () => {
document.querySelectorAll('.svf-dd-list').forEach(l => l.style.display = 'none');
});
}
function init() {
const section = document.querySelector(cfg.sectionSelector);
if (!section || section.dataset.svfInit) return;
const domItems = section.querySelectorAll('.product-list-item');
if (domItems.length === 0) return;
section.dataset.svfInit = 'true';
injectCSS();
fetchStoreData(window.location.pathname)
.then(data => {
processData(data, domItems);
buildUI(section);
const firstSpinner = document.querySelector('.svf-spinner');
if(firstSpinner) firstSpinner.style.display = 'none';
});
}
setTimeout(init, 500);
})();
</script>

#2. Customize
#2.1. You can change variant dropdown style at Line 32 – Line 41
dropdown: {
minWidth: '150px',
background: '#ffffff',
border: '1px solid #e5e5e5',
borderRadius: '3px',
textSize: '14px',
textColor: '#333333',
panelBackground: '#ffffff',
hoverBackground: '#f5f5f5'
}