Multi Filter Setup (Multi Match)
Advanced filter system where multiple buttons can be active simultaneously. Supports AND/OR item matching, a toggleable Reset button, and items belonging to multiple categories. Configured entirely via data attributes on the group container.
filtermulti-selectand/oranimationjavascriptwebflow
Code
HTML
html
<!-- data-filter-target-match="multi" → multiple buttons can be active at once -->
<!-- data-filter-name-match="single" → item must match ALL active tags (AND logic) -->
<!-- Use "multi" for name-match to show items matching ANY active tag (OR logic) -->
<div data-filter-group="" data-filter-target-match="multi" data-filter-name-match="single" role="group" class="filter-group">
<div class="filter-buttons">
<button data-filter-target="all" data-filter-status="active" aria-pressed="false" class="filter-btn">All</button>
<button data-filter-target="orange" data-filter-status="not-active" aria-pressed="false" class="filter-btn">Orange</button>
<button data-filter-target="blue" data-filter-status="not-active" aria-pressed="false" class="filter-btn">Blue</button>
<button data-filter-target="green" data-filter-status="not-active" aria-pressed="false" class="filter-btn">Green</button>
<button data-filter-target="brown" data-filter-status="not-active" aria-pressed="false" class="filter-btn">Brown</button>
<!-- Reset: hidden when showing all, visible when a filter is active -->
<button data-filter-target="reset" data-filter-status="not-active" aria-pressed="false" class="reset-btn">Reset</button>
</div>
<div aria-live="polite" role="list" class="filter-list">
<!-- Single-tag 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="green" class="demo-card__tags-item">
<p class="demo-card__tags-item-p">Green</p>
</div>
</div>
</div>
</div>
<div class="demo-card__bottom">
<h3 class="demo-card__h3">Frog</h3>
</div>
</div>
</div>
<!-- Multi-tag item (Parrot: green + orange) -->
<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="green" class="demo-card__tags-item"><p class="demo-card__tags-item-p">Green</p></div>
<div data-filter-name-collect="orange" class="demo-card__tags-item"><p class="demo-card__tags-item-p">Orange</p></div>
</div>
</div>
</div>
<div class="demo-card__bottom">
<h3 class="demo-card__h3">Parrot</h3>
</div>
</div>
</div>
<!-- Unicorn: orange + blue + green -->
<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="orange" class="demo-card__tags-item"><p class="demo-card__tags-item-p">Orange</p></div>
<div data-filter-name-collect="blue" class="demo-card__tags-item"><p class="demo-card__tags-item-p">Blue</p></div>
<div data-filter-name-collect="green" class="demo-card__tags-item"><p class="demo-card__tags-item-p">Green</p></div>
</div>
</div>
</div>
<div class="demo-card__bottom">
<h3 class="demo-card__h3">Unicorn</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;
}
/* Reset Button — hidden when showing all, visible when a filter is active */
.reset-btn {
outline-offset: -2px;
color: #c90f0f;
-webkit-appearance: none;
appearance: none;
background-color: #c90f0f0d;
border-radius: 10em;
outline: 2px solid #c90f0f;
padding: .65em 1.25em;
font-size: 1.5em;
transition: all 0.6s cubic-bezier(0.625, 0.05, 0, 1);
opacity: 0;
visibility: hidden;
}
.reset-btn[data-filter-status="active"] {
opacity: 1;
visibility: visible;
}
/* 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 initMutliFilterSetupMultiMatch() {
const transitionDelay = 300;
const groups = [...document.querySelectorAll('[data-filter-group]')];
groups.forEach(group => {
const targetMatch = (group.getAttribute('data-filter-target-match') || 'multi').trim().toLowerCase();
const nameMatch = (group.getAttribute('data-filter-name-match') || 'multi').trim().toLowerCase();
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 collectors = item.querySelectorAll('[data-filter-name-collect]');
if (!collectors.length) return;
const seen = new Set(), tokens = [];
collectors.forEach(c => {
const v = (c.getAttribute('data-filter-name-collect') || '').trim().toLowerCase();
if (v && !seen.has(v)) { seen.add(v); tokens.push(v); }
});
if (tokens.length) item.setAttribute('data-filter-name', tokens.join(' '));
});
// Cache item tokens
const itemTokens = new Map();
items.forEach(el => {
const raw = (el.getAttribute('data-filter-name') || '').trim().toLowerCase();
itemTokens.set(el, new Set(raw ? raw.split(/\s+/).filter(Boolean) : []));
});
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');
}
};
// Active tags model
let activeTags = targetMatch === 'single' ? null : new Set(['all']);
const hasRealActive = () => {
if (targetMatch === 'single') return activeTags !== null;
return activeTags.size > 0 && !activeTags.has('all');
};
const resetAll = () => {
if (targetMatch === 'single') {
activeTags = null;
} else {
activeTags.clear();
activeTags.add('all');
}
};
// AND vs OR item matching
const itemMatches = el => {
if (!hasRealActive()) return true;
const tokens = itemTokens.get(el);
if (targetMatch === 'single') return tokens.has(activeTags);
const selected = [...activeTags];
if (nameMatch === 'single') {
// AND: item must contain all selected tags
return selected.every(t => tokens.has(t));
} else {
// OR: item must contain at least one selected tag
return selected.some(t => tokens.has(t));
}
};
const paint = rawTarget => {
const target = (rawTarget || '').trim().toLowerCase();
if ((target === 'all' || target === 'reset') && !hasRealActive()) return;
if (target === 'all' || target === 'reset') {
resetAll();
} else if (targetMatch === 'single') {
activeTags = target;
} else {
if (activeTags.has('all')) activeTags.delete('all');
if (activeTags.has(target)) activeTags.delete(target);
else activeTags.add(target);
if (activeTags.size === 0) resetAll();
}
// Update items with transition
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);
}
});
// Update buttons
buttons.forEach(btn => {
const t = (btn.getAttribute('data-filter-target') || '').trim().toLowerCase();
let on = false;
if (t === 'all') on = !hasRealActive();
else if (t === 'reset') on = hasRealActive();
else on = targetMatch === 'single' ? activeTags === t : activeTags.has(t);
setButtonState(btn, on);
});
};
group.addEventListener('click', e => {
const btn = e.target.closest('[data-filter-target]');
if (btn && group.contains(btn)) paint(btn.getAttribute('data-filter-target'));
});
paint('all');
});
}
// Initialize Multi Filter Setup (Multi Match)
document.addEventListener('DOMContentLoaded', () => {
initMutliFilterSetupMultiMatch();
});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-match | "single" | "multi" | "multi" | "multi" allows multiple buttons to be active simultaneously. "single" allows only one active button at a time. |
| data-filter-name-match | "single" | "multi" | "multi" | "single" = AND logic — item must contain ALL active tags. "multi" = OR logic — item must contain ANY active tag. |
| data-filter-target | string | — | On a button — the category token it toggles. Use "all" to show everything, "reset" for a contextual reset button. No spaces — use hyphens for multi-word values. |
| data-filter-name | string | — | On a list item — space-separated tokens. Auto-populated from [data-filter-name-collect] children if left empty. |
| data-filter-name-collect | string | — | Add to child elements to auto-build the parent's [data-filter-name] from unique tokens. Designed for Webflow CMS collection lists. |
| data-filter-status | active | not-active | transition-out | — | Tracks visibility state on both buttons and items. "transition-out" is temporarily applied during exit animation. |
| transitionDelay | number (ms) | 300 | JS constant — duration items stay in "transition-out" before hiding. Match to your CSS transition duration. |
Notes
- •The key difference from Basic Filter: multiple buttons can be active at once (data-filter-target-match="multi").
- •AND mode (data-filter-name-match="single"): only items matching ALL selected tags appear — ideal for attribute stacking.
- •OR mode (data-filter-name-match="multi"): items matching ANY selected tag appear — broader, more inclusive results.
- •The Reset button (data-filter-target="reset") is hidden via CSS when no filter is active and visible when one is — opposite of the All button.
- •Items can belong to multiple tags — separate tokens with spaces in [data-filter-name], or use [data-filter-name-collect] children.
- •No spaces in token values — use hyphens: data-filter-target="blue-birds".
- •Multiple independent filter groups on the same page are fully supported.
- •Related components: Basic Filter Setup and Basic Filter Setup (Multi Match).