CSS でフィルタリング(絞り込み) JavaScript でも

CSS だけを使って表示されている要素のフィルタリング(絞り込み・コンテンツフィルター)を実装する方法や表示する際にアニメーションを追加する方法、及び JavaScript を使って実装する方法等の覚書です。

下記サイトのページを参考にさせていただきました。

How to Build a Filtering Component in Pure CSS

作成日:2021年6月15日

関連ページ:CSS Grid + JavaScript で Masonry レイアウト(フィルタリングの追加)

CSS だけでコンテンツをフィルタリング

CSS のみを使ってコンテンツをフィルタリングする例です。

以下のようにラジオボタンを選択すると、ラベルに表示された内容と一致するコンテンツを表示します。

以下の例では表示するコンテンツは単に div 要素にスタイルを設定したものですが、同様の方法で画像ギャラリーやカードなどをフィルタリングすることができます。

サンプル(別ページで開く)

以下が上記の例の HTML のマークアップです。

フィルタのラジオボタンを1つのグループとして認識させるため name 属性に共通の名前(categories)を各 input 要素に指定します。

ラベルをクリックしてもラジオボタンが選択されるように(明示的なラベル付け)するため、label 要素の for 属性にラジオボタンの id 属性と同じ値を指定します。

初期状態で「全て(All)」のラジオボタンが選択されるように最初の input 要素に checked 属性を指定しています。

ラジオボタンの value 属性にはコンテンツをフィルタリングするための値(カテゴリ名)を指定します。

コンテンツ側(フィルタリングされる要素)ではカスタムデータ属性 data-category にフィルタリングで使用される値(該当するカテゴリ名:ラジオボタンの value 属性の値)を半角スペース区切りで指定します。

※コンテンツ側の class 属性はその要素のスタイル(色と形)を設定するもので、フィルタリングには関係ありません。

HTML
 <div class="sample">
  <!-- フィルタ(ラジオボタンとラベル) -->
  <input type="radio" name="categories" id="All" value="All" checked>
  <label for="All"> 全て </label>
  <input type="radio" name="categories" id="Blue" value="Blue">
  <label for="Blue"> ブルー </label>
  <input type="radio" name="categories" id="Green" value="Green">
  <label for="Green"> グリーン </label>
  <input type="radio" name="categories" id="Red" value="Red">
  <label for="Red"> レッド </label>
  <input type="radio" name="categories" id="Square" value="Square">
  <label for="Square"> 正方形 </label>
  <input type="radio" name="categories" id="Circle" value="Circle">
  <label for="Circle"> 円 </label>

  <!-- フィルタリングする対象のコンテンツ -->
  <ol class="targets">
    <li class="target" data-category="Square">
      <div class="square"></div>
    </li>
    <li class="target" data-category="Circle">
      <div class="circle"></div>
    </li>
    <li class="target" data-category="Blue Square">
      <div class="blue square"></div>
    </li>
    <li class="target" data-category="Blue Circle">
      <div class="blue circle"></div>
    </li>
    <li class="target" data-category="Green Square">
      <div class="green square"></div>
    </li>
    <li class="target" data-category="Green Circle">
      <div class="green circle"></div>
    </li>
    <li class="target" data-category="Red Square">
      <div class="red square"></div>
    </li>
    <li class="target" data-category="Red Circle">
      <div class="red circle"></div>
    </li>
  </ol>
</div>

CSS

以下がフィルタリング部分の CSS です。

仕組みとしては初期状態では記述されている全ての要素(コンテンツ)が表示されています。

All 以外のフィルタ(ラジオボタン)をクリックするとラジオボタンが選択された状態(:checked)になり、対応(該当)するフィルタ以外の要素(:not([data-category~="xxxx"]))を非表示にすることで、対応するフィルタの要素のみが表示されます(4〜10行目)。

All のフィルタをクリックすると全ての要素(全ての data-category 属性を持つ要素)が表示されます。

CSS (フィルタリング部分の抜粋)
[value="All"]:checked ~ .targets [data-category] {
  display: block;
}
[value="Blue"]:checked ~ .targets .target:not([data-category~="Blue"]), 
[value="Green"]:checked ~ .targets .target:not([data-category~="Green"]), 
[value="Red"]:checked ~ .targets .target:not([data-category~="Red"]), 
[value="Square"]:checked ~ .targets .target:not([data-category~="Square"]), 
[value="Circle"]:checked ~ .targets .target:not([data-category~="Circle"]) {
  display: none;
}

上記の CSS では次のセレクタを組み合わせて使用しています。

属性セレクタ

[value="xxxx"]は value 属性の値が xxxx の要素を表します。

[data-category~="xxxx"]は value 属性の値が空白文字区切りで複数指定されている場合に、その属性値が含まれている要素を表します。「~」の代わりに「*」を使って[data-category*="xxxx"] (指定した属性値が部分一致)とすることもできます。

:checked 擬似クラス

:checked はラジオボタンやチェックボックスがチェック(選択)された場合に適用されます。

:not 擬似クラス

指定されたセレクタ以外のセレクタに適用されるセレクタです。

間接セレクタ

間接セレクタは「~(チルダ)」結合子で連結して指定する「親要素が同じになる兄弟関係の弟に適用される」セレクタで、このセレクタの使い方がこのフィルタリング機能のポイントになります。

例えば、上記 CSS 抜粋の4行目は、 value 属性の値が Blue のラジオボタンが選択された場合に、その兄弟関係の弟の「.targets .target」で data-category 属性の値に Blue が含まれていない(:not)要素を非表示にするというような意味になります。

※ この仕組の場合、間接セレクタを使ってフィルタリングの機能を実装しているので、例えばラジオボタンの input 要素のグループを div 要素で囲むなどしてコンテンツの要素との兄弟関係がなくなってしまうと機能しなくなってしまいます。そのような構造にする場合は、JavaScript で実装するなどを検討します。

レイアウト

この例では、フィルタリングの対象のコンテンツのレイアウトは CSS グリッド(display:grid)を使用しています。

.targets {
  display: grid;
  grid-gap: 40px;
  grid-template-columns: repeat(auto-fit, 60px);
  margin-top: 40px;
}
ol {
  list-style: none;
}
a {
  text-decoration: none;
  color: inherit;
}
  
.targets {
  display: grid;
  grid-gap: 40px;
  grid-template-columns: repeat(auto-fit, 60px);
  margin-top: 40px;
}
.circle, .square {
  width: 60px;
  height: 60px;
  background-color: #EEE;
  border: 1px solid #CCC;
}
.circle {
  border-radius: 50%;
}
.blue {
  background-color: #B8DBF6;
  border: 1px solid #5ABEED;
}
.green {
  background-color: #C8F8D1;
  border: 1px solid #64D994;
}
.red {
  background-color: #FAD6D7
}
[value="All"]:checked ~ .targets [data-category] {
  display: block;
}
[value="Blue"]:checked ~ .targets .target:not([data-category~="Blue"]), 
[value="Green"]:checked ~ .targets .target:not([data-category~="Green"]), 
[value="Red"]:checked ~ .targets .target:not([data-category~="Red"]),
[value="Square"]:checked ~ .targets .target:not([data-category~="Square"]), 
[value="Circle"]:checked ~ .targets .target:not([data-category~="Circle"]) {
  display: none;
}

ラジオボタンを非表示に

以下はラジオボタンを非表示にして、ラベルをクリックしてフィルタリングするようにした例です。

サンプル(別ページで開く)

以下が上記の例の HTML のマークアップです。

この例では、フィルタのラベルを ol 要素で記述して分離しています。但し、input 要素とコンテンツとの関係は前述の例同様、兄弟関係はそのままです。

その他の部分は前述の例と同じです。

<div class="sample">
  <input type="radio" name="categories" id="All" value="All" checked>
  <input type="radio" name="categories" id="Blue" value="Blue">
  <input type="radio" name="categories" id="Green" value="Green">
  <input type="radio" name="categories" id="Red" value="Red">
  <input type="radio" name="categories" id="Square" value="Square">
  <input type="radio" name="categories" id="Circle" value="Circle">
  <!-- ラベルを分離 -->
  <ol class="filters">
    <li><label for="All"> All </label></li>
    <li><label for="Blue"> Blue </label></li>
    <li><label for="Green"> Green </label></li>
    <li><label for="Red"> Red </label></li>
    <li><label for="Square"> Square </label></li>
    <li><label for="Circle"> Circle </label></li>
  </ol>
  <ol class="targets">
    <li class="target" data-category="Square">
      <div class="square"></div>
    </li>
    <li class="target" data-category="Circle">
      <div class="circle"></div>
    </li>
    <li class="target" data-category="Blue Square">
      <div class="blue square"></div>
    </li>
    <li class="target" data-category="Blue Circle">
      <div class="blue circle"></div>
    </li>
    <li class="target" data-category="Green Square">
      <div class="green square"></div>
    </li>
    <li class="target" data-category="Green Circle">
      <div class="green circle"></div>
    </li>
    <li class="target" data-category="Red Square">
      <div class="red square"></div>
    </li>
    <li class="target" data-category="Red Circle">
      <div class="red circle"></div>
    </li>
  </ol>
</div>

以下が前述の例の CSS と異なる(追加した)部分です。

ラジオボタンを絶対配置にして位置を左側に 9999px 移動して非表示にしています。

その他はラベル部分のスタイルの設定です。

機能の部分は前述の例と同じです。

CSS(追加部分抜粋)
/* ラジオボタンを非表示に */
input[type="radio"] {
  position: absolute;
  left: -9999px;
}
/* 以下はラベルのスタイル */
.filters {
  margin-bottom: 2rem;
}
.filters * {
  display: inline-block;
}
.filters label {
  text-align: center;
  padding: 0.25rem 0.5rem;
  margin-bottom: 0.25rem;
  min-width: 50px;
  line-height: normal;
  cursor: pointer;
  transition: all 0.2s;
}
.filters label:hover {
  background: #333;
  color: #fff;
}
[value="All"]:checked ~ .filters [for="All"], 
[value="Blue"]:checked ~ .filters [for="Blue"], 
[value="Green"]:checked ~ .filters [for="Green"], 
[value="Red"]:checked ~ .filters [for="Red"], 
[value="Square"]:checked ~ .filters [for="Square"], 
[value="Circle"]:checked ~ .filters [for="Circle"] {
  background: #333;
  color: #fff;
}
ol {
  list-style: none;
}
a {
  text-decoration: none;
  color: inherit;
}
.targets {
  display: grid;
  grid-gap: 40px;
  grid-template-columns: repeat(auto-fit, 60px);
  margin-top: 40px;
}
.circle, .square {
  width: 60px;
  height: 60px;
  background-color: #EEE;
  border: 1px solid #CCC;
}
.circle {
  border-radius: 50%;
}
.blue {
  background-color: #B8DBF6;
  border: 1px solid #5ABEED;
}
.green {
  background-color: #C8F8D1;
  border: 1px solid #64D994;
}
.red {
  background-color: #FAD6D7
}
[value="All"]:checked ~ .targets [data-category] {
  display: block;
}
[value="Blue"]:checked ~ .targets .target:not([data-category~="Blue"]), 
[value="Green"]:checked ~ .targets .target:not([data-category~="Green"]), 
[value="Red"]:checked ~ .targets .target:not([data-category~="Red"]), 
[value="Square"]:checked ~ .targets .target:not([data-category~="Square"]), 
[value="Circle"]:checked ~ .targets .target:not([data-category~="Circle"]) {
  display: none;
}
/*  ラジオボタンを非表示に */
input[type="radio"] {
  position: absolute;
  left: -9999px;
}
/*  以下はラベルのスタイル */
.filters {
  margin-bottom: 2rem;
}
.filters * {
  display: inline-block;
}
.filters label {
  text-align: center;
  padding: 0.25rem 0.5rem;
  margin-bottom: 0.25rem;
  min-width: 50px;
  line-height: normal;
  cursor: pointer;
  transition: all 0.2s;
}
.filters label:hover {
  background: #333;
  color: #fff;
}
[value="All"]:checked ~ .filters [for="All"], 
[value="Blue"]:checked ~ .filters [for="Blue"], 
[value="Green"]:checked ~ .filters [for="Green"], 
[value="Red"]:checked ~ .filters [for="Red"], 
[value="Square"]:checked ~ .filters [for="Square"], 
[value="Circle"]:checked ~ .filters [for="Circle"] {
  background: #333;
  color: #fff;
}

JavaScript でフィルタリング

前述の例のように CSS だけでフィルタリングが可能ですが、この例の場合間接セレクタ(~)を使っているため、例えば以下のようにラジオボタンの input 要素のグループを div 要素で囲んでしますとコンテンツの要素との兄弟関係がなくなってしまうので機能しなくなります。

<div class="sample">
  <div class="filters"> <!-- input 要素のグループを div 要素で囲む -->
    <input type="radio" name="categories" id="All" value="All" checked>
    <label for="All"> All </label>
    <input type="radio" name="categories" id="Blue" value="Blue">
    <label for="Blue"> Blue </label>
    <input type="radio" name="categories" id="Green" value="Green">
    <label for="Green"> Green </label>
    <input type="radio" name="categories" id="Red" value="Red">
    <label for="Red"> Red </label>
    <input type="radio" name="categories" id="Square" value="Square">
    <label for="Square"> Square </label>
    <input type="radio" name="categories" id="Circle" value="Circle">
    <label for="Circle"> Circle </label>
  </div>
  <ol class="targets">
    <li class="target" data-category="Square">
      <div class="square"></div>
    </li>
    <li class="target" data-category="Circle">
      <div class="circle"></div>
    </li>
    <li class="target" data-category="Blue Square">
      <div class="blue square"></div>
    </li>
    <li class="target" data-category="Blue Circle">
      <div class="blue circle"></div>
    </li>
    <li class="target" data-category="Green Square">
      <div class="green square"></div>
    </li>
    <li class="target" data-category="Green Circle">
      <div class="green circle"></div>
    </li>
    <li class="target" data-category="Red Square">
      <div class="red square"></div>
    </li>
    <li class="target" data-category="Red Circle">
      <div class="red circle"></div>
    </li>
  </ol>
</div>

以下は CSS でのフィルタリングとほぼ同じことを JavaScript で実装する例です。

この場合、ラジオボタンの input 要素とコンテンツの要素との位置関係に依存しません(兄弟関係でなくても機能します)。

また、以下の JavaScript で前述の CSS のフィルタリングの例の全てに対応することができます。

window.addEventListener('DOMContentLoaded', function(){
  //name 属性が categories の input 要素(ラジオボタン)の集まり(静的な NodeList)を取得
  const input_categories = document.querySelectorAll("input[name=categories]");
  //全ての .target の要素(target クラスを指定された div 要素)を取得
  const targets = document.querySelectorAll(".target");
  
  //ループで各ラジオボタンにイベントリスナを設定
  for(let input_category of input_categories) {
    //change イベントリスナを各ラジオボタンに登録
    input_category.addEventListener('change',function(){    
      for(let target of targets) {
        //全ての .target の要素に display: block; を設定
        target.style.setProperty('display', 'block');
      }
      //ラジオボタンが選択された場合
      if( this.checked ) {
        //ラジオボタンの value 属性が All 以外の場合
        if(this.value !== 'All') {
          //target クラスの要素で data-category 属性にこのラジオボタンの value 属性の値が含まれていないものを全て取得
          const not_checked_categories = document.querySelectorAll('.target:not([data-category~=' + '"' + this.value + '"])');
          //取得した要素に display: none を設定して非表示に
          for(let not_checked_category of not_checked_categories) {
            not_checked_category.style.setProperty('display', 'none');
          }
        }
      }
    });
  }     
});

おおまかな内容は、ラジオボタンが選択された際(イベント発生時)に全てのコンテンツ(target クラスを指定された div 要素)に display: block; を設定し、All のラジオボタン以外が選択された場合は、その選択されたラジオボタンの value 属性の値を含まないコンテンツ全てに display: none を設定して非表示にしています。内容的には CSS のフィルタリングとほぼ同じことをしています。

関連ページ

以下は CSS です(CSS でのフィルタリング部分を削除しているだけです)。

CSS
* {
  margin: 0 auto;
  padding: 0;
}
ol {
  list-style: none;
}
a {
  text-decoration: none;
  color: inherit;
}
.targets {
  display: grid;
  grid-gap: 40px;
  grid-template-columns: repeat(auto-fit, 60px);
  margin-top: 40px;
}
.circle, .square {
  width: 60px;
  height: 60px;
  background-color: #EEE;
  border: 1px solid #CCC;
}
.circle {
  border-radius: 50%;
}
.blue {
  background-color: #B8DBF6;
  border: 1px solid #5ABEED;
}
.green {
  background-color: #C8F8D1;
  border: 1px solid #64D994;
}
.red {
  background-color: #FAD6D7
}

サンプル(別ページで開く)

サンプル(ラジオボタンを非表示 JavaScript 版)

サンプル(一覧表示・ギャラリー JavaScript 版)

アニメーションの追加

以下は CSS アニメーションのクラスを作成して、フィルタが選択された際に JavaScript でそのクラスを追加することでアニメーションを実行させる例です。

サンプル(別ページで開く)

以下がアニメーションを追加した JavaScript です。※ この例の場合、CSS でアニメーションを追加した方がシンプルで、以下はあまり良い例ではないと思います(次項参照)。

最初に .target の全ての要素からアニメーションのクラスを削除して、ラジオボタン(フィルタ)が選択されたら、選択されたフィルタの値を含む data-category を持つ .target の要素にアニメーションのクラスを追加しています。

All が選択された場合は、全ての .target の要素にアニメーションのクラスを追加しています。

window.addEventListener('DOMContentLoaded', function(){
  const input_categories = document.querySelectorAll("input[name=categories]");
  const targets = document.querySelectorAll(".target");
  
  for(let input_category of input_categories) {
    input_category.addEventListener('change',function(){     
      for(let target of targets) {
        target.style.setProperty('display', 'block');
        //全ての要素からアニメーションのクラスを削除
        target.classList.remove('checked_animation');
      }
      if( this.checked ) {
        if(this.value !== 'All') {
          const not_checked_categories = document.querySelectorAll('.target:not([data-category~=' + '"' + this.value + '"])');
          for(let not_checked_category of not_checked_categories) {
            not_checked_category.style.setProperty('display', 'none');
          }
          //data-category に選択されたラジオボタンの value 属性の値が含まれる .target の要素にアニメーションのクラスを追加 
          const checked_categories = document.querySelectorAll('.target[data-category~=' + '"' + this.value + '"]');
          for(let checked_category of checked_categories) {
            checked_category.classList.add('checked_animation');
          } 
        }else{
          //選択されたラジオボタンの value 属性の値が All の場合は全ての .target の要素にアニメーションのクラスを追加 
          for(let target of targets) {
            target.classList.add('checked_animation');
          }
        }
      }
    });
  }     
});

以下がアニメーションのクラス(.checked_animation)です。

/* アニメーション */
.checked_animation {
  animation: checked_animation 0.4s ease-in-out both;
}
 
@keyframes checked_animation {
  0% {
  transform: translate(0, 300px);
  opacity: 0;
  }
  100% {
  transform: translate(0, 0);
  opacity: 1;
  }
}

上記の JavaScript と CSS によるアニメーションの追加は、単純に CSS に以下を記述(.target の div 要素にアニメーションを設定)するのとほぼ同じです(上記の場合は初期表示でアニメーションが実行されませんが)。

.target {
  animation: checked_animation 0.4s ease-in-out both;
}
@keyframes checked_animation {
  0% {
  transform: translate(0, 300px);
  opacity: 0;
  }
  100% {
  transform: translate(0, 0);
  opacity: 1;
  }
}
animationend イベント

前述の例の場合、単に CSS でアニメーションを設定したのと同様、現在表示されている(すでにアニメーションが一度実行された)コンテンツはアニメーションが実行されません。

要素の animationend イベントを使って、アニメーション終了時に classList から一度実行したアニメーションのクラスを削除すれば毎回アニメーションが実行されるようになります。

サンプル(別ページで開く)

HTML と CSS は同じなので省略します。

JavaScript では animationend イベントを使って、アニメーション終了時に各要素からアニメーションのクラスを削除するようにしています(5〜11行目)。それ以外は前述の例と同じです。

JavaScript
window.addEventListener('DOMContentLoaded', function(){
  const input_categories = document.querySelectorAll("input[name=categories]");
  const targets = document.querySelectorAll(".target");
  
  for(let target of targets) {
    //animationend イベントを追加
    target.addEventListener('animationend', () => {
      // アニメーション終了後にクラスを削除
      target.classList.remove('checked_animation');
    });
  }
  
  for(let input_category of input_categories) {
    input_category.addEventListener('change',function(){     
      for(let target of targets) {
        target.style.setProperty('display', 'block');
        //全ての要素からアニメーションのクラスを削除
        target.classList.remove('checked_animation');
      }
      if( this.checked ) {
        if(this.value !== 'All') {
          const not_checked_categories = document.querySelectorAll('.target:not([data-category~=' + '"' + this.value + '"])');
          for(let not_checked_category of not_checked_categories) {
            not_checked_category.style.setProperty('display', 'none');
          }
          //data-category に選択されたラジオボタンの value 属性の値が含まれる .target の要素にアニメーションのクラスを追加 
          const checked_categories = document.querySelectorAll('.target[data-category~=' + '"' + this.value + '"]');
          for(let checked_category of checked_categories) {
            checked_category.classList.add('checked_animation');
          } 
        }else{
          //選択されたラジオボタンの value 属性の値が All の場合は全ての .target の要素にアニメーションのクラスを追加 
          for(let target of targets) {
            target.classList.add('checked_animation');
          }
        }
      }
    });
  }     
});
JavaScript で CSS アニメーションを設定

以下は CSS アニメーションを CSS で記述するのではなく、JavaScript を使って style 要素に出力する例です。

HTML は前述までの例と同じで、CSS は前述の例のアニメーションの部分を削除します。

この例では、以下のようにそれぞれのコンテンツに適用するアニメーションの translate に指定する値や遅延の値を少しずつ異なるようにしています(CSS でそれぞれ異なるアニメーションを設定・記述するのは大変ですが、JavaScript を使えば比較的簡単に設定できます)。

JavaScript で出力する CSS
.checked_animation-0 {
  animation: checked_animation-0 0.2s ease-in-out 0s both;  
}

@keyframes checked_animation-0 { 
  0% {
  transform: translate(-100px, 100px);
  opacity: 0;
  }
  100%  {
  transform: translate(0, 0);
  opacity: 1;
  }
}
.checked_animation-1 {
  animation: checked_animation-1 0.2s ease-in-out 0.03333333333333333s both;
}

@keyframes checked_animation-1 { 
  0% {
  transform: translate(-110px, 110px);
  opacity: 0;
  }
  100%  {
  transform: translate(0, 0);
  opacity: 1;
  }
}

・・・中略・・・

.checked_animation-11 {
    animation: checked_animation-11 0.2s ease-in-out 0.36666666666666664s both;
  }

  @keyframes checked_animation-11 { 
    0% {
    transform: translate(210px, 210px);
    opacity: 0;
    }
    100%  {
    transform: translate(0, 0);
    opacity: 1;
    }
  }</style>

  

上記の CSS を手動で設定するのは大変なので、JavaScript でテンプレートリテラルを使って変数に入れた値を出力するようにしています。

ルール部分の抜粋
const animation_rules = `
  .${animation_class} {
    animation: ${animation_name} 0.2s ease-in-out ${animation_delay} both;
  }

  @keyframes ${animation_name} { 
    0% {
    transform: translate(${translate_from_x}, ${translate_from_y});
    opacity: 0;
    }
    100%  {
    transform: translate(0, 0);
    opacity: 1;
    }
  }`  

サンプル(別ページで開く)

前半部分は前述までの例とほぼ同じですが、以下が異なります。

7行目は取得した全ての .target の要素の集合 NodeList(変数 targets)から配列を作成しています。

10行目は作成するアニメーションのクラス名の固定部分の文字列を変数に代入しておきます。

17行目ではアニメーションのクラスを削除する際に、アニメーションのクラス名に要素のインデックスを付けていますが、これはそれぞれの要素に指定するアニメーションを作成する際(83行目)にインデックスを付けているためです。

また、29行目では現在処理している要素のインデックスを取得するために、7行目で作成した配列を使っています(何番目の要素かを取得)。

46行目以降がアニメーションの CSS を作成して style 要素に出力する部分です。

キーフレームアニメーションのルールを生成して返す関数を定義して、全ての .target の要素に対して for 文(81〜109行目)でキーフレームアニメーションを設定しています。

そして style 要素を生成し、作成したルールを設定して head 要素に追加しています(style タグを追加)。

アニメーションの遅延の値や translate で指定する値などは70〜78行目で適当に設定しています。これらの値を変更することでアニメーションの動きが変わります。

この例では、animation-duration は固定の 0.2s(49行目)にしていますが、別途変数を追加して for 文(118〜146行目)の中でこの値を変更することもできます。

<script>
window.addEventListener('DOMContentLoaded', function(){
  const input_categories = document.querySelectorAll("input[name=categories]");
  //全ての .target の要素(target クラスを指定された div 要素)を取得
  const targets = document.querySelectorAll('.target');
  // NodeList から配列を作成
  targets_array = Array.prototype.slice.call( targets ) ;
  //または targets_array = [].slice.call( targets ) ;
  //アニメーションのクラス名の固定部分の文字列
  const animationClassNamePrefix = 'checked_animation-';
  
  for(let input_category of input_categories) {
    input_category.addEventListener('change',function(){
      for(let i=0; i<targets.length; i++) {
        targets[i].style.setProperty('display', 'block');
        //全ての要素からアニメーションのクラスを削除
        targets[i].classList.remove(animationClassNamePrefix + i);
      }
      if( this.checked ) {
        if(this.value !== 'All') {
          const not_checked_categories = document.querySelectorAll('.target:not([data-category~=' + '"' + this.value + '"])');
          for(let not_checked_category of not_checked_categories) {
            not_checked_category.style.setProperty('display', 'none');
          }
          //data-category に選択されたラジオボタンの value 属性の値が含まれる .target の要素にアニメーションのクラスを追加 
          const checked_categories = document.querySelectorAll('.target[data-category~=' + '"' + this.value + '"]');
          for(let checked_category of checked_categories) {
            //この要素が全ての.target の要素の中の何番目かを取得
            const index = targets_array.indexOf( checked_category );	
            //取得したインデックスを使ってアニメーション用のクラスを追加
            checked_category.classList.add(animationClassNamePrefix + index);
          }  
        }else{
          //選択されたラジオボタンの value 属性の値が All の場合は全ての .target の要素にアニメーションのクラスを追加 
          for(let i=0; i<targets.length; i++) {
            //全ての要素からアニメーションのクラスを追加
            targets[i].classList.add(animationClassNamePrefix + i);
          }
        }
      }
    });
  }   
  
  //アニメーション
  //キーフレームアニメーションのルールを生成して返す関数
  const returnAnimation  = (animation_class, animation_name, animation_delay, translate_from_x, translate_from_y) => {
    const animation_rules = `
  .${animation_class} {
    animation: ${animation_name} 0.2s ease-in-out ${animation_delay} both;
  }

  @keyframes ${animation_name} { 
    0% {
    transform: translate(${translate_from_x}, ${translate_from_y});
    opacity: 0;
    }
    100%  {
    transform: translate(0, 0);
    opacity: 1;
    }
  }`  
    return animation_rules;
  };
  
  //CSS ルールの文字列の初期化
  let rules = '';
  //ウィンドウ幅
  const ww = window.innerWidth;
  //アニメーションの開始遅延を調整する値(大きくすれば遅延は小さくなる)
  const start_delay_control = 30;
  //transform: translate() の水平方向の初期値
  const x_init_value = 100;
  //transform: translate() の垂直方向の初期値
  const y_init_value = 100;
  //transform: translate() 水平方向を調整する値
  const x_multiple = 10;
  //transform: translate() の垂直方向を調整する値
  const y_multiple = 10;
  
  //全ての target クラスを指定した div 要素(targets の各要素)にアニメーションを設定
  for(let i = 0; i < targets.length; i++ ) {
    //要素に指定するアニメーションを用のクラス名
    const animation_class = animationClassNamePrefix + i;
    //アニメーションの名前(クラス名と同じにしているが、異なる名前でも問題なし)
    const animation_name = animationClassNamePrefix + i;
    //アニメーションの開始遅延(i の値を元に調整。)
    const animation_delay = i/start_delay_control + 's';
    //ウィンドウ幅をその要素の要素の水平方向のオフセット座標で割った値
    const x = ww/targets[i].offsetLeft;
    //translate に指定する x 座標の値の初期値
    let translate_from_x = '0px';
    //上記で取得した x の値が2より大きければに x 座標の値を負の値に
    if(x < 2) {
      translate_from_x = (x_init_value + i * x_multiple) + 'px'; 
    }else{
      translate_from_x =  '-' + (x_init_value + i * x_multiple) + 'px';
    }
    //translate に指定する y 座標の値
    const translate_from_y = (y_init_value + i * y_multiple) + 'px';
    //アニメーションのクラスを追加
    targets[i].classList.add(animation_class);
    //animationend イベントを設定
    targets[i].addEventListener('animationend', () => {
      // アニメーション終了後にクラスを削除
      targets[i].classList.remove(animation_class);
    });
    //関数 returnAnimation() を使ってルールの文字列を作成(追加)
    rules += `${returnAnimation (animation_class, animation_name, animation_delay, translate_from_x, translate_from_y)}`;
  }
  
  //style 要素を作成
  const s = document.createElement( 'style' );
  //style 要素にルールを設定
  s.innerHTML = rules;
  //head 要素の末尾に上記で作成した style 要素を追加
  document.getElementsByTagName( 'head' )[ 0 ].appendChild( s ); 
   
});
</script>

出力する CSS を圧縮(改行・スペースの削除)

上記の場合、head 要素内に追加される style 要素に記述される CSS は改行されています。アニメーションのルールを生成して返す関数 returnAnimation のルールを作成する部分(上記46〜63行目)を以下のように改行やスペースは削除すると良いかもしれません。

const returnAnimation  = (animation_class, animation_name, animation_delay, translate_from_x, translate_from_y) => {
    const animation_rules = `.${animation_class} {animation: ${animation_name} 0.2s ease-in-out ${animation_delay} both;}@keyframes ${animation_name} {0% {transform:translate(${translate_from_x}, ${translate_from_y});opacity: 0;}100% {transform: translate(0, 0);opacity:1;}}`  
    return animation_rules;
  };

サンプル(別ページで開く)

インスペクタで確認すると以下のように表示されます。

ラベルにカウント数を表示

フィルタのラベルに対象のコンテンツ(要素)のカウント数を表示する例です。

サンプル(別ページで開く)

以下が JavaScript になります。HTML のマークアップや CSS は前述の例と同じです。

JavaScript の前述の例と異なる部分は、7行目のカテゴリ名を格納する配列の初期化、23行目のカテゴリ名を配列に追加する処理、及び26行目以降になります。

詳細はコメントに記述してありますが、おおまかな内容としてはラジオボタンの value 属性の値(カテゴリ名)を取得し、その値をキーとする連想配列(オブジェクト)を作成して、それぞれの要素にカテゴリが使われている数をカウントして値に設定しています(もっと良い方法があるかも知れません)。

但し、All の場合はコンテンツの要素にカテゴリとして All は使われていないので常に 0 になるので、.target(コンテンツの要素)の総数を設定します。

window.addEventListener('DOMContentLoaded', function(){
  //name 属性が categories の input 要素(ラジオボタン)を取得
  const input_categories = document.querySelectorAll("input[name=categories]");
  //全ての .target の要素(target クラスを指定された div 要素)を取得
  const targets = document.querySelectorAll(".target");
  //ラジオボタンの value の値(カテゴリ名)を格納する配列
  const category_array = [];
  for(let input_category of input_categories) {
    input_category.addEventListener('change',function(){   
      for(let target of targets) {
        target.style.setProperty('display', 'block');
      }
      if( this.checked ) {
        if(this.value !== 'All') {
          const not_checked_categories = document.querySelectorAll('.target:not([data-category~=' + '"' + this.value + '"])');
          for(let not_checked_category of not_checked_categories) {
            not_checked_category.style.setProperty('display', 'none');
          }
        }
      }
    });
    //ラジオボタンの value の値を配列(category_array)に追加
    category_array.push(input_category.getAttribute('value'));
  } 
  
  //カテゴリ名(ラジオボタンの value の値)をキーとする連想配列(オブジェクト)の初期化
  const category_vars = {};
  //カテゴリ名をキーとする連想配列の要素を生成し値(カウント数)に初期値 0 を設定
  for(let cat of category_array){
     category_vars[cat] = 0;
  }
  
  //[data-category] 属性を持つ要素を取得
  const data_categories = document.querySelectorAll("[data-category]");
  //それぞれの要素の data-category の値を取得し、それぞれの値をカウントアップ
  for(let data_categoriy of data_categories){
    //data-category の値を取得
    let category_values = data_categoriy.getAttribute('data-category');
    //data-category の値を半角スペース(空白文字の正規表現)で分割
    let category_values_array = category_values.split(/\s/);
    //分割された data-category の値をキーとした連想配列の値をカウントアップ
    for(let category_value of category_values_array) {
      category_vars[category_value] ++;
    }
  }
  
  //ラジオボタンの value の値(カテゴリ名)から label 要素を取得してカウント数を表示
  for(let input_category of input_categories) {
    let input_value = input_category.getAttribute('value');
    //for 属性が value の値に一致する要素(label 要素)
    let label = document.querySelector('[for="'+ input_value + '"]');
    //label 要素のテキスト(ラベル)
    let label_text = label.textContent;
    if(input_value === 'All') {
      //All の場合に表示する値は .target の総数
      label.textContent = label_text + ' (' + document.querySelectorAll(".target").length +  ')';
    }else{
      //All以外の場合は value の値(カテゴリ名)をキーに持つ連想配列の値(カテゴリのカウント数)
      label.textContent = label_text + ' (' + category_vars[input_value] +  ')';
    }
  }
});

Bootstrap のツールチップで表示

Bootstrap を使っている場合、前述とほぼ同じ方法でフィルタのラベルに対象のコンテンツ(要素)のカウント数をツールチップで表示することができます。

以下の例ではラベルにマウスオーバーすると該当する要素の数が表示されます。

サンプル(別ページで開く)

この例で使用する Bootstrap のバージョンは 5.0 です。また、この例では CDN 経由で Bootstrap を読み込んでいます(Bootstrap 5 Introduction)。

<head> 内で他のスタイルシートよりも先に CSS を読み込みます
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
</body> の直前で JavaScript を読み込みます。
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"></script>

以下が JavaScript になります。HTML のマークアップや CSS は前述の例と同じです。

前述の例と異なる部分は 47行目以降になります。

前述の例ではラベルのテキストを変更しましたが、この例の場合は Bootstrap の属性(data-bs-xxxx)を追加しています。

ツールチップに表示される値はデフォルトでは title 属性に指定しますが、この例では data-bs-original-title 属性に指定しています。

65〜68行目は Bootstrap のツールチップを利用する場合に必要な初期化処理です。

window.addEventListener('DOMContentLoaded', function(){
  //name 属性が categories の input 要素(ラジオボタン)を取得
  const input_categories = document.querySelectorAll("input[name=categories]");
  //全ての .target の要素(target クラスを指定された div 要素)を取得
  const targets = document.querySelectorAll(".target");
  //ラジオボタンの value の値(カテゴリ名)を格納する配列
  const category_array = [];
  for(let input_category of input_categories) {
    input_category.addEventListener('change',function(){
      for(let target of targets) {
        target.style.setProperty('display', 'block');
      }
      if( this.checked ) {
        if(this.value !== 'All') {
          const not_checked_categories = document.querySelectorAll('.target:not([data-category~=' + '"' + this.value + '"])');
          for(let not_checked_category of not_checked_categories) {
            not_checked_category.style.setProperty('display', 'none');
          }
        }
      }
    });
    //ラジオボタンの value の値を配列(category_array)に追加
    category_array.push(input_category.getAttribute('value'));
  } 
  
  //カテゴリ名(ラジオボタンの value の値)をキーとする連想配列(オブジェクト)の初期化
  const category_vars = {};
  //カテゴリ名をキーとする連想配列の要素を生成し値(カウント数)に初期値 0 を設定
  for(let cat of category_array){
     category_vars[cat] = 0;
  }
  
  //[data-category] 属性を持つ要素を取得
  const data_categories = document.querySelectorAll("[data-category]");
  //それぞれの要素の data-category の値を取得し、それぞれの値をカウントアップ
  for(let data_categoriy of data_categories){
    //data-category の値を取得
    let category_values = data_categoriy.getAttribute('data-category');
    //data-category の値を半角スペース(空白文字の正規表現)で分割
    let category_values_array = category_values.split(/\s/);
    //分割された data-category の値をキーとした連想配列の値をカウントアップ
    for(let category_value of category_values_array) {
      category_vars[category_value] ++;
    }
  }
  
  //ラジオボタンの value の値を元に label 要素を取得してツールチップに使用する属性を設定
  for(let input_category of input_categories) {
    let input_value = input_category.getAttribute('value');
    //for 属性が value の値に一致する要素(label 要素)
    let label = document.querySelector('[for="'+ input_value + '"]');
    if(input_value === 'All') {
      //All の場合はツールチップに表示する値は .target の総数
      label.setAttribute('data-bs-original-title', document.querySelectorAll(".target").length); 
    }else{
      //All以外の場合は value の値(カテゴリ名)をキーに持つ連想配列の値(カテゴリのカウント数)
      label.setAttribute('data-bs-original-title', category_vars[input_value]); 
    }
    // ツールチップに必要な属性を設定
    label.setAttribute('data-bs-toggle', 'tooltip');
    label.setAttribute('data-bs-placement', 'top');
  }
  
  //ページ上のすべてのツールチップを初期化(Bootstrap)
  var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
  var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
    return new bootstrap.Tooltip(tooltipTriggerEl)
  })
  
}); 

上記の JavaScript により、フィルタ(label 要素)部分は以下のように出力されます。

<ol class="filters">
  <li><label for="All" data-bs-original-title="12" data-bs-toggle="tooltip" data-bs-placement="top">All</label></li>
  <li><label for="cat-a" data-bs-original-title="5" data-bs-toggle="tooltip" data-bs-placement="top">カテゴリA</label></li>
  <li><label for="cat-b" data-bs-original-title="3" data-bs-toggle="tooltip" data-bs-placement="top">カテゴリB</label></li>
  <li><label for="cat-c" data-bs-original-title="4" data-bs-toggle="tooltip" data-bs-placement="top">カテゴリC</label></li>
  <li><label for="cat-d" data-bs-original-title="4" data-bs-toggle="tooltip" data-bs-placement="top">カテゴリD</label></li>
  <li><label for="cat-e" data-bs-original-title="3" data-bs-toggle="tooltip" data-bs-placement="top">カテゴリE</label></li>
  <li><label for="cat-f" data-bs-original-title="3" data-bs-toggle="tooltip" data-bs-placement="top">カテゴリF</label></li>
</ol>

関連ページ:Webpack を使って Bootstrap 5 をインストール(バンドル)