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

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

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

How to Build a Filtering Component in Pure CSS

作成日:2021年6月15日

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]");
  //ループで各ラジオボタンにイベントリスナを設定
  for(let input_category of input_categories) {
    //change イベントリスナを各ラジオボタンに登録
    input_category.addEventListener('change',function(){
      //全ての .target の要素(target クラスを指定された div 要素)を取得
      const targets = document.querySelectorAll(".target");
      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]");
  for(let input_category of input_categories) {
    input_category.addEventListener('change',function(){
      const targets = document.querySelectorAll(".target");
      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;
  }
}

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

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

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

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

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

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

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

window.addEventListener('DOMContentLoaded', function(){
  //name 属性が categories の input 要素(ラジオボタン)を取得
  const input_categories = document.querySelectorAll("input[name=categories]");
  //ラジオボタンの value の値(カテゴリ名)を格納する配列
  const category_array = [];
  for(let input_category of input_categories) {
    input_category.addEventListener('change',function(){
      const targets = document.querySelectorAll(".target");
      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 は前述の例と同じです。

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

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

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

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

window.addEventListener('DOMContentLoaded', function(){
  //name 属性が categories の input 要素(ラジオボタン)を取得
  const input_categories = document.querySelectorAll("input[name=categories]");
  //ラジオボタンの value の値(カテゴリ名)を格納する配列
  const category_array = [];
  for(let input_category of input_categories) {
    input_category.addEventListener('change',function(){
      const targets = document.querySelectorAll(".target");
      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 をインストール(バンドル)