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

NameTypeDefaultDescription
data-filter-groupattributeWrapper 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-targetstringOn 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-namestringOn a list item — space-separated tokens. Auto-populated from [data-filter-name-collect] children if left empty.
data-filter-name-collectstringAdd to child elements to auto-build the parent's [data-filter-name] from unique tokens. Designed for Webflow CMS collection lists.
data-filter-statusactive | not-active | transition-outTracks visibility state on both buttons and items. "transition-out" is temporarily applied during exit animation.
transitionDelaynumber (ms)300JS 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).