<div id="filter-ui" class="pane">
  <div>
    <h2>Tabs</h2>
    <div class="pane">
      <ul>
        <li><a href="#" class="js-tab is-current" data-category="all">All</a></li>
        <li><a href="#" class="js-tab" data-category="alpha">Category Alpha</a></li>
        <li><a href="#" class="js-tab" data-category="beta">Category Beta</a></li>
        <li><a href="#" class="js-tab" data-category="3">Category 3</a></li>
        <li><a href="#" class="js-tab" data-category="4">Category 4</a></li>
        <li><a href="#" class="js-tab" data-category="5">Category 5</a></li>
      </ul>
      <ul>
        <li><a href="#" class="js-tab is-current" data-category="all">All</a></li>
        <li><a href="#" class="js-tab" data-category="alpha">Category Alpha</a></li>
        <li><a href="#" class="js-tab" data-category="beta">Category Beta</a></li>
        <li><a href="#" class="js-tab" data-category="3">Category 3</a></li>
        <li><a href="#" class="js-tab" data-category="4">Category 4</a></li>
        <li><a href="#" class="js-tab" data-category="5">Category 5</a></li>
      </ul>
    </div>

    <h2>Selects</h2>
    <select class="js-select">
      <option value="all">All</option>
      <option value="alpha">Alpha</option>
      <option value="beta">Beta</option>
      <option value="3">3</option>
      <option value="4">4</option>
      <option value="5">5</option>
    </select>
    <select class="js-select">
      <option value="all">All</option>
      <option value="alpha">Alpha</option>
      <option value="beta">Beta</option>
      <option value="3">3</option>
      <option value="4">4</option>
      <option value="5">5</option>
    </select>
    <select class="js-select">
      <option value="all">All</option>
      <option value="alpha">Alpha</option>
      <option value="beta">Beta</option>
      <option value="3">3</option>
      <option value="4">4</option>
      <option value="5">5</option>
    </select>
  </div>

  <div>
    <h2>Items</h2>
    <ol class="items">
      <li class="js-item" data-category="3">Item Category 3</li>
      <li class="js-item" data-category="3">Item Category 3</li>
      <li class="js-item" data-category="alpha">Item Category Alpha</li>
      <li class="js-item" data-category="beta">Item Category Beta</li>
      <li class="js-item" data-category="4">Item Category 4</li>
      <li class="js-item" data-category="beta">Item Category Beta</li>
      <li class="js-item" data-category="3">Item Category 3</li>
      <li class="js-item" data-category="alpha">Item Category Alpha</li>
      <li class="js-item" data-category="beta">Item Category Beta</li>
      <li class="js-item" data-category="3">Item Category 3</li>
      <li class="js-item" data-category="beta">Item Category Beta</li>
      <li class="js-item" data-category="alpha">Item Category Alpha</li>
      <li class="js-item" data-category="alpha">Item Category Alpha</li>
      <li class="js-item" data-category="alpha">Item Category Alpha</li>
      <li class="js-item" data-category="4">Item Category 4</li>
      <li class="js-item" data-category="3">Item Category 3</li>
      <li class="js-item" data-category="alpha">Item Category Alpha</li>
      <li class="js-item" data-category="beta">Item Category Beta</li>
      <li class="js-item" data-category="5">Item Category 5</li>
      <li class="js-item" data-category="5">Item Category 5</li>
      <li class="js-item" data-category="5">Item Category 5</li>
      <li class="js-item" data-category="4">Item Category 4</li>
    </ol>
  </div>

</div>
* {
  transition: color .3s ease, opacity .3s ease;
}

.is-current {
  color: red;
}

.is-hidden {
  opacity: 0.3;
  // display: none;
}

.pane {
  display: flex;
  flex-flow: row nowrap;
}

.items {
  display: flex;
  flex-flow: column wrap;
  height: 75vh;
  list-style: none;
  padding: 0;
  
  > li {
    padding: 1em;
  }
}
View Compiled
class FilterUI {

  constructor($container) {

    // ルート要素が無効または存在しないときはエラー
    if (typeof $container === 'undefined' || $container instanceof HTMLElement === false) {
      throw new Error('$container is not available!');
    }

    // ルート要素
    this.$container = $container;

    // タブ要素
    this.$tabs = null;

    // セレクト要素
    this.$selects = null;

    // リスト要素
    this.$items = null;

    // 現在のカテゴリ名
    this.currentCategory = '';

    // データ属性名
    this.dataKey = 'category';

    // タブ要素を検索するためのセレクタ名
    this.tabsSelector = '.js-tab';

    // セレクト要素を検索するためのセレクタ名
    this.selectsSelector = '.js-select';

    // リスト要素を検索するためのセレクタ名
    this.itemsSelector = '.js-item';

    // is-hiddenクラス
    this.hiddenClass = 'is-hidden';

    // is-currentクラス
    this.activeClass = 'is-current';

    // 「すべて」を意味するカテゴリ名
    this.allCategory = 'all';
  }

  // 初期化
  init() {
    // (最初の状態では、タブとセレクタは静的に辻褄が合っている前提がある)

    // ルート要素からタブ要素を取得する
    this.$tabs = this.$container.querySelectorAll(this.tabsSelector);

    // タブ要素にイベントリスナonClickを付与
    [].forEach.call(this.$tabs, ($tab) => {
      $tab.addEventListener('click', this.onClick.bind(this, $tab));
    });

    // ルート要素からselect要素を取得する
    this.$selects = this.$container.querySelectorAll(this.selectsSelector);

    // select要素にイベンリスナonChangeを付与
    [].forEach.call(this.$selects, ($select) => {
      $select.addEventListener('change', this.onChange.bind(this, $select));
    });
    
    // ルート要素からリスト要素を取得する
    this.$items = this.$container.querySelectorAll(this.itemsSelector);
  }

  // onClickイベントリスナ
  onClick($el, e) {
    e.preventDefault();

    // クリックされた要素のdata-categoryから次のカテゴリ名を取得する
    const nextCategory = $el.dataset[this.dataKey];

    // 取得した値で カテゴリ変更 を実行
    this.changeCategory(nextCategory);
  }

  // onChangeイベントリスナ
  onChange($el) {

    // 変更されたselect要素から現在のvalue値を取得する
    const nextCategory = $el.value;

    // 取得した値で カテゴリ変更 を実行
    this.changeCategory(nextCategory);
  }

  // カテゴリ変更
  changeCategory(nextCategory) {

    // (タブ側、セレクト側の両方から変更が発生することを想定しておく)

    // 次のカテゴリ名と現在のカテゴリ名を比較する
    // 次カテゴリ名と現在のカテゴリ名に差分がない場合は何もしない
    if (nextCategory === this.currentCategory) return;

    // 次のカテゴリ名を現在のカテゴリ名として保存する
    this.currentCategory = nextCategory;

    // タブの状態を更新する を実行
    this.updateTabs(nextCategory);

    // select要素の状態を更新する を実行
    this.updateSelects(nextCategory);

    // リストの状態を更新する を実行
    this.updateItems(nextCategory);
  }


  // タブの状態を更新する
  updateTabs(nextCategory) {

    // eachで要素ごとに繰り返し
    [].forEach.call(this.$tabs, ($tab) => {

      // タブ(個別)の状態を更新する を実行
      this.updateTab(nextCategory, $tab);

    });
  }

  // タブ(個別)の状態を更新する
  updateTab(nextCategory, $tab) {
  
    // すべての項目から .is-current を除去
    $tab.classList.remove(this.activeClass);

    // 次のカテゴリ名を持つ項目に .is-current をセット
    if ($tab.dataset[this.dataKey] !== nextCategory) return;
    $tab.classList.add(this.activeClass);

  }

  // select要素の状態を更新する
  updateSelects(nextCategory) {

    // eachで要素ごとに繰り返し
    [].forEach.call(this.$selects, ($select) => {

      // select要素(個別)の状態を更新する を実行
      this.updateSelect(nextCategory, $select);

    });
  }

  // select要素(個別)の状態を更新する
  updateSelect(nextCategory, $select) {

    // valueに次のカテゴリ名をセットする
    $select.value = nextCategory;
    
  }


  // リストの状態を更新する
  updateItems(nextCategory) {

    // eachで要素ごとに繰り返し
    [].forEach.call(this.$items, ($item) => {

      // リスト更新 を実行
      this.updateItem(nextCategory, $item);

    });

  }

  // リスト更新
  updateItem(nextCategory, $item) {

    // .is-hidden を除去
    $item.classList.remove(this.hiddenClass);

    // カテゴリ名が すべて のときはここで終了
    if (nextCategory === this.allCategory) return;

    // 対象物の data-category がカテゴリ名とマッチするかを比較
    // 一致しない場合は .is-hidden を付与
    if ($item.dataset[this.dataKey] === nextCategory) return;
    $item.classList.add(this.hiddenClass);

  }

}

const $el = document.getElementById('filter-ui');
const filterUi = new FilterUI($el);
filterUi.init();
View Compiled
Rerun