Basic Filter Setup (Multi Match)
Attribute-driven filter system where items can belong to multiple categories simultaneously. Buttons control which items are visible, with smooth transition animations and an optional "All" button to reset the view.
filtermulti-matchanimationjavascriptwebflow
Code
HTML
html
<div data-filter-group="" role="group" class="filter-group">
<div class="filter-buttons">
<button data-filter-target="all" data-filter-status="active" aria-pressed="false" aria-controls="filter-list" class="filter-btn">All</button>
<button data-filter-target="land" data-filter-status="not-active" aria-pressed="false" aria-controls="filter-list" class="filter-btn">Land</button>
<button data-filter-target="water" data-filter-status="not-active" aria-pressed="false" aria-controls="filter-list" class="filter-btn">Water</button>
<button data-filter-target="air" data-filter-status="not-active" aria-pressed="false" aria-controls="filter-list" class="filter-btn">Air</button>
</div>
<div aria-live="polite" role="list" class="filter-list">
<!-- Single-category item -->
<div role="listitem" data-filter-name="" data-filter-status="active" class="filter-list__item">
<div class="demo-card">
<div class="demo-card__top">
<div class="demo-card__visual">
<div class="demo-card__visual-before"></div>
<span class="demo-card__emoji">🦁</span>
</div>
<div class="demo-card__tags-collection">
<div class="demo-card__tags-list">
<div data-filter-name-collect="land" class="demo-card__tags-item">
<p class="demo-card__tags-item-p">Land</p>
</div>
</div>
</div>
</div>
<div class="demo-card__bottom">
<h3 class="demo-card__h3">Lion</h3>
</div>
</div>
</div>
<!-- Multi-category item (Penguin: water + land) -->
<div role="listitem" data-filter-name="" data-filter-status="active" class="filter-list__item">
<div class="demo-card">
<div class="demo-card__top">
<div class="demo-card__visual">
<div class="demo-card__visual-before"></div>
<span class="demo-card__emoji">🐧</span>
</div>
<div class="demo-card__tags-collection">
<div class="demo-card__tags-list">
<div data-filter-name-collect="water" class="demo-card__tags-item">
<p class="demo-card__tags-item-p">Water</p>
</div>
<div data-filter-name-collect="land" class="demo-card__tags-item">
<p class="demo-card__tags-item-p">Land</p>
</div>
</div>
</div>
</div>
<div class="demo-card__bottom">
<h3 class="demo-card__h3">Penguin</h3>
</div>
</div>
</div>
<!-- Duck: land + water + air -->
<div role="listitem" data-filter-name="" data-filter-status="active" class="filter-list__item">
<div class="demo-card">
<div class="demo-card__top">
<div class="demo-card__visual">
<div class="demo-card__visual-before"></div>
<span class="demo-card__emoji">🦆</span>
</div>
<div class="demo-card__tags-collection">
<div class="demo-card__tags-list">
<div data-filter-name-collect="land" class="demo-card__tags-item">
<p class="demo-card__tags-item-p">Land</p>
</div>
<div data-filter-name-collect="water" class="demo-card__tags-item">
<p class="demo-card__tags-item-p">Water</p>
</div>
<div data-filter-name-collect="air" class="demo-card__tags-item">
<p class="demo-card__tags-item-p">Air</p>
</div>
</div>
</div>
</div>
<div class="demo-card__bottom">
<h3 class="demo-card__h3">Duck</h3>
</div>
</div>
</div>
<!-- Repeat pattern for additional items -->
</div>
</div>CSS
css
.filter-group {
min-height: 100vh;
padding-bottom: 10em;
}
/* Filter Buttons */
.filter-buttons {
grid-column-gap: .5em;
grid-row-gap: .5em;
flex-flow: wrap;
justify-content: flex-start;
padding: 1em 1em 3em;
display: flex;
}
.filter-btn {
-webkit-appearance: none;
appearance: none;
background-color: #efeeec;
border-radius: 10em;
padding: .65em 1.25em;
font-size: 1.5em;
transition: color 0.6s cubic-bezier(0.625, 0.05, 0, 1),
background-color 0.6s cubic-bezier(0.625, 0.05, 0, 1);
}
.filter-btn[data-filter-status="active"] {
background-color: #131313;
color: #EFEEEC;
}
/* Filter List */
.filter-list {
flex-flow: wrap;
width: 100%;
display: flex;
}
.filter-list__item {
width: 25%;
padding: .75em;
}
.filter-list__item[data-filter-status="active"] {
transition: opacity 0.6s cubic-bezier(0.625, 0.05, 0, 1),
transform 0.6s cubic-bezier(0.625, 0.05, 0, 1);
transform: scale(1) rotate(0.001deg);
opacity: 1;
visibility: visible;
position: relative;
}
.filter-list__item[data-filter-status="transition-out"] {
transition: opacity 0.45s cubic-bezier(0.625, 0.05, 0, 1),
transform 0.45s cubic-bezier(0.625, 0.05, 0, 1);
transform: scale(0.9) rotate(0.001deg);
opacity: 0;
visibility: visible;
}
.filter-list__item[data-filter-status="not-active"] {
transform: scale(0.9) rotate(0.001deg);
opacity: 0;
visibility: hidden;
position: absolute;
}
/* Demo Card */
.demo-card {
grid-column-gap: 1em;
grid-row-gap: 1em;
background-color: #efeeec;
border-radius: 1.5em;
flex-flow: column;
width: 100%;
padding: 1em;
display: flex;
}
.demo-card__top { position: relative; }
.demo-card__bottom {
justify-content: flex-start;
align-items: center;
padding-bottom: .25em;
padding-left: .5em;
padding-right: .5em;
display: flex;
}
.demo-card__h3 {
margin-top: 0;
margin-bottom: 0;
font-size: 1.25em;
font-weight: 500;
line-height: 1;
}
.demo-card__visual {
background-color: #e2dfdf;
border-radius: .5em;
justify-content: center;
align-items: center;
width: 100%;
display: flex;
position: relative;
}
.demo-card__visual-before { padding-top: 66%; }
.demo-card__emoji { font-size: 4em; }
.demo-card__tags-collection {
width: 100%;
padding: 1em;
position: absolute;
top: 0;
left: 0;
}
.demo-card__tags-list { display: flex; }
.demo-card__tags-item {
background-color: #efeeec;
border-radius: 3em;
padding: .25em .75em;
}
.demo-card__tags-item-p {
margin-bottom: 0;
font-size: .875em;
}
@media screen and (max-width: 991px) {
.filter-list__item { width: 50%; }
}
@media screen and (max-width: 767px) {
.filter-list__item { width: 100%; }
}JavaScript
javascript
function initBasicFilterSetupMultiMatch() {
const transitionDelay = 300;
const groups = [...document.querySelectorAll('[data-filter-group]')];
groups.forEach(group => {
const buttons = [...group.querySelectorAll('[data-filter-target]')];
const items = [...group.querySelectorAll('[data-filter-name]')];
// Auto-build data-filter-name from [data-filter-name-collect] children
items.forEach(item => {
const cs = item.querySelectorAll('[data-filter-name-collect]');
if (!cs.length) return;
const seen = new Set(), out = [];
cs.forEach(c => {
const v = (c.getAttribute('data-filter-name-collect') || '').trim().toLowerCase();
if (v && !seen.has(v)) { seen.add(v); out.push(v); }
});
if (out.length) item.setAttribute('data-filter-name', out.join(' '));
});
// Cache tokens per item
const itemTokens = new Map();
items.forEach(el => {
const tokens = (el.getAttribute('data-filter-name') || '')
.trim().toLowerCase().split(/\s+/).filter(Boolean);
itemTokens.set(el, new Set(tokens));
});
const setItemState = (el, on) => {
const next = on ? 'active' : 'not-active';
if (el.getAttribute('data-filter-status') !== next) {
el.setAttribute('data-filter-status', next);
el.setAttribute('aria-hidden', on ? 'false' : 'true');
}
};
const setButtonState = (btn, on) => {
const next = on ? 'active' : 'not-active';
if (btn.getAttribute('data-filter-status') !== next) {
btn.setAttribute('data-filter-status', next);
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
}
};
let activeTarget = null;
const itemMatches = el => {
if (!activeTarget || activeTarget === 'all') return true;
return itemTokens.get(el).has(activeTarget);
};
const paint = rawTarget => {
const target = (rawTarget || '').trim().toLowerCase();
activeTarget = (!target || target === 'all') ? 'all' : target;
items.forEach(el => {
if (el._ft) clearTimeout(el._ft);
const next = itemMatches(el);
const cur = el.getAttribute('data-filter-status');
if (cur === 'active' && transitionDelay > 0) {
el.setAttribute('data-filter-status', 'transition-out');
el._ft = setTimeout(() => { setItemState(el, next); el._ft = null; }, transitionDelay);
} else if (transitionDelay > 0) {
el._ft = setTimeout(() => { setItemState(el, next); el._ft = null; }, transitionDelay);
} else {
setItemState(el, next);
}
});
buttons.forEach(btn => {
const t = (btn.getAttribute('data-filter-target') || '').trim().toLowerCase();
setButtonState(btn, (activeTarget === 'all' && t === 'all') || (t && t === activeTarget));
});
};
group.addEventListener('click', e => {
const btn = e.target.closest('[data-filter-target]');
if (btn && group.contains(btn)) paint(btn.getAttribute('data-filter-target'));
});
});
}
// Initialize Basic Filter Setup (Multi Match)
document.addEventListener('DOMContentLoaded', () => {
initBasicFilterSetupMultiMatch();
});Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| data-filter-group | attribute | — | Wrapper that scopes buttons and items together. Multiple independent groups on the same page are supported. |
| data-filter-target | string | — | On a button — the category to show when clicked. Use "all" to show everything. No spaces: multi-word values use hyphens (e.g. "blue-birds"). |
| data-filter-name | string | — | On a list item — space-separated tokens matching button targets. Auto-populated by [data-filter-name-collect] if left empty. |
| data-filter-name-collect | string | — | Optional. Add to child elements (e.g. Webflow collection items) to auto-build the parent's [data-filter-name] without duplicates. |
| data-filter-status | active | not-active | transition-out | — | Tracks visibility state. "active" = visible. "not-active" = hidden. "transition-out" = temporarily applied during the exit animation before hiding. |
| transitionDelay | number (ms) | 300 | JS constant controlling how long items stay in "transition-out" state before becoming "not-active". Match this to your CSS transition duration. |
Notes
- •Items can belong to multiple categories — separate tokens with spaces in [data-filter-name], e.g. data-filter-name="water land".
- •The [data-filter-name-collect] system is optional and designed for Webflow CMS collections, where each collection item can output one token.
- •The "All" button uses the special value [data-filter-target="all"] and shows every item regardless of their tokens.
- •No spaces in token values — use hyphens for multi-word categories: data-filter-target="blue-birds".
- •Multiple independent filter groups on the same page are fully supported.
- •The transition-out state allows CSS animations to complete before the item is hidden — match transitionDelay to your CSS transition duration.
- •Related components: Basic Filter Setup (single-match) and Multi Filter Setup (Multi Match).