CMS Filter and Search in Webflow with Vanilla JS (No Library) using 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 listif (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 listsortBtn.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.
Jared Malan