Basic Filter Setup
Simple single-select filter system. One button is active at a time; clicking it shows only items whose [data-filter-name] matches the button's [data-filter-target]. Includes smooth transition animations and an "All" button to reset.
filtersingle-selectanimationjavascriptwebflow
Code
HTML
html
<div role="group" data-filter-group="" class="filter-group">
<div class="filter-buttons">
<button data-filter-target="all" data-filter-status="active" aria-pressed="true" class="filter-btn">All</button>
<button data-filter-target="red-fruit" data-filter-status="not-active" aria-pressed="false" class="filter-btn">Red</button>
<button data-filter-target="yellow-fruit" data-filter-status="not-active" aria-pressed="false" class="filter-btn">Yellow</button>
<button data-filter-target="other" data-filter-status="not-active" aria-pressed="false" class="filter-btn">Other</button>
</div>
<div aria-live="polite" role="list" class="filter-list">
<div role="listitem" data-filter-name="red-fruit" 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>
<div class="demo-card__bottom">
<h3 class="demo-card__h3">Watermelon</h3>
</div>
</div>
</div>
<div role="listitem" data-filter-name="yellow-fruit" 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>
<div class="demo-card__bottom">
<h3 class="demo-card__h3">Banana</h3>
</div>
</div>
</div>
<div role="listitem" data-filter-name="other" 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>
<div class="demo-card__bottom">
<h3 class="demo-card__h3">Coconut</h3>
</div>
</div>
</div>
<!-- Repeat pattern: set data-filter-name to match a button's data-filter-target -->
</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__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; }
@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 initFilterBasic() {
const groups = document.querySelectorAll('[data-filter-group]');
groups.forEach((group) => {
const buttons = group.querySelectorAll('[data-filter-target]');
const items = group.querySelectorAll('[data-filter-name]');
const transitionDelay = 300; // ms — match your CSS transition duration
const updateStatus = (element, shouldBeActive) => {
element.setAttribute('data-filter-status', shouldBeActive ? 'active' : 'not-active');
element.setAttribute('aria-hidden', shouldBeActive ? 'false' : 'true');
};
const handleFilter = (target) => {
items.forEach((item) => {
const shouldBeActive = target === 'all' || item.getAttribute('data-filter-name') === target;
const currentStatus = item.getAttribute('data-filter-status');
if (currentStatus === 'active') {
item.setAttribute('data-filter-status', 'transition-out');
setTimeout(() => updateStatus(item, shouldBeActive), transitionDelay);
} else {
setTimeout(() => updateStatus(item, shouldBeActive), transitionDelay);
}
});
buttons.forEach((button) => {
const isActive = button.getAttribute('data-filter-target') === target;
button.setAttribute('data-filter-status', isActive ? 'active' : 'not-active');
button.setAttribute('aria-pressed', isActive ? 'true' : 'false');
});
};
buttons.forEach((button) => {
button.addEventListener('click', () => {
const target = button.getAttribute('data-filter-target');
if (button.getAttribute('data-filter-status') === 'active') return;
handleFilter(target);
});
});
});
}
// Initialize Basic Filter Setup
document.addEventListener('DOMContentLoaded', () => {
initFilterBasic();
});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 activate when clicked. Use "all" to show every item. No spaces: use hyphens for multi-word values (e.g. "red-fruit"). |
| data-filter-name | string | — | On a list item — must exactly match a button's [data-filter-target]. Items are shown only when their value matches the active button. No spaces. |
| data-filter-status | active | not-active | transition-out | — | "active" = visible. "not-active" = hidden. "transition-out" = temporarily applied during exit animation before hiding. |
| transitionDelay | number (ms) | 300 | JS constant — how long items stay in "transition-out" before hiding. Match this value to your CSS transition duration. |
Notes
- •Only one button can be active at a time — clicking an already-active button does nothing.
- •Each item's [data-filter-name] must exactly match one button's [data-filter-target]. No partial or multi-value matching in this basic version.
- •Use hyphens for multi-word filter names: data-filter-target="red-fruit" / data-filter-name="red-fruit".
- •The transition-out state lets CSS animations finish before items are hidden — match transitionDelay to your CSS duration.
- •For items belonging to multiple categories, use Basic Filter Setup (Multi Match) instead.
- •For multiple simultaneously active filters, use Multi Filter Setup (Multi Match) instead.