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

NameTypeDefaultDescription
data-filter-groupattributeWrapper that scopes buttons and items together. Multiple independent groups on the same page are supported.
data-filter-targetstringOn 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-namestringOn 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-statusactive | not-active | transition-out"active" = visible. "not-active" = hidden. "transition-out" = temporarily applied during exit animation before hiding.
transitionDelaynumber (ms)300JS 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.