CMS Filter and Search in Webflow with Vanilla JS (No Library) using Slater
Here’s a lightweight, library-free pattern for CMS search and multi-tag filtering on Webflow—scoped safely and deployable via Slater.
You can add fast, client-side search and multi-tag filtering to a Webflow Collection List with a few data attributes and a small vanilla JS module published from Slater.
What you’ll build
- Text search that filters items based on visible text.
- Multi-tag filtering with checkboxes (AND logic) or a select.
- Safe scoping so it only runs on pages that contain your list.
Assumed markup (Designer)
- Wrap your Collection List with an attribute: [data-cms="list"]. Each item gets [data-cms="item"].
- Add a text input: [data-filter="search"].
- Add tag checkboxes: inputs with [data-filter="tag"] and value="tag-slug". Each item has data-tags="tag-1,tag-2".
Vanilla JS module (paste into a Slater file)
(function initCmsFilter() {
const root = document.querySelector('[data-cms="list"]');
if (!root) return; // page doesn’t have the list
if (root.dataset.inited) return; // avoid double init
root.dataset.inited = '1';
const items = Array.from(root.querySelectorAll('[data-cms="item"]'));
const state = { q: '', tags: new Set() };
function itemMatches(el) {
const text = el.textContent.toLowerCase();
if (state.q && !text.includes(state.q)) return false;
if (state.tags.size) {
const tags = (el.dataset.tags || '')
.toLowerCase()
.split(',')
.map(s => s.trim())
.filter(Boolean);
for (const t of state.tags) if (!tags.includes(t)) return false; // AND logic
}
return true;
}
function render() {
for (const el of items) el.style.display = itemMatches(el) ? '' : 'none';
}
const search = document.querySelector('[data-filter="search"]');
if (search) search.addEventListener('input', (e) => {
state.q = e.target.value.toLowerCase().trim();
render();
});
document.addEventListener('change', (e) => {
const cb = e.target;
if (cb && cb.matches('[data-filter="tag"]')) {
const v = (cb.value || '').toLowerCase();
cb.checked ? state.tags.add(v) : state.tags.delete(v);
render();
}
});
render();
})();
Optional: sorting (A→Z)
(function addSortAZ() {
const root = document.querySelector('[data-cms="list"]');
const sortBtn = document.querySelector('[data-sort="az"]');
if (!root || !sortBtn) return;
const wrapper = root.parentElement; // container that holds the list
sortBtn.addEventListener('click', () => {
const items = Array.from(root.querySelectorAll('[data-cms="item"]'))
.sort((a, b) => a.textContent.localeCompare(b.textContent));
const frag = document.createDocumentFragment();
items.forEach(i => frag.appendChild(i));
root.appendChild(frag);
});
})();
Performance and UX tips
- Keep selectors stable by using data attributes; avoid brittle class selectors.
- For very large lists, debounce the search handler or pre-index text content.
- If items become dynamic at runtime, wrap your init in a MutationObserver to re-run render when new items appear.
Publishing
- Add the Slater snippet once at site level in Webflow.
- Publish your filter script to Staging first; validate on webflow.io; then promote to Production.
FAQ
- Can I use OR logic for tags? Yes—replace the AND loop with an intersection check that returns true if any tag matches.
- Can I fetch CMS data via API instead? You will need to make a server side API call which is not possible with Slater. In most use cases are covered by client-side filtering of the already-rendered list.
- Will this break Webflow Interactions? No—this hides items via style.display. If interactions target hidden items, test and adjust your triggers.