こんにちは、株式会社ファストコーディングのBigViです。この前、ネットで服を探していました。「Tシャツ・Mサイズ」で検索したら「243件」と出ました。。「白」を追加したら、一瞬で「31件」に変わりました。でも数字がパッと切り替わっただけだったので、「あれ、ちゃんとフィルターかかった?」ってちょっと不安でした。
先日、お客さまから「サイト内検索でフィルターを変えても、ユーザーが結果件数の変化に気づいていない」と相談されました。フィルターで絞り込んでも、ユーザーがスクロールして結果を探してしまう。件数表示を見ていないみたいです。
私もよくあります。検索フィルターを操作したのに、結果が変わったかどうか確信が持てないこと。数字を見ても「さっきと同じ?」ってなります。。
今回は「検索結果の件数が変わったとき、0.3秒だけ数値ロール風の演出をつける」という実装をしました。数字がクルッと回るように見える、小さな動きです。
なぜ「数値ロール」なのか
検索結果の件数は、ユーザーにとって大事な情報です。「128件→23件」に減ったなら、フィルターが効いているとわかる。「23件→0件」になったなら、条件が厳しすぎるとわかる。
でも、数字がパッと切り替わるだけだと気づかないことがあります。特にフィルターが複数あるサイトでは、ユーザーの目はフィルターのUIに集中しています。件数表示は視界の端にあることが多い。
数値ロール(数字が上下に回転するように見える演出)は、変化を「動き」で伝えます。パッと変わるよりも、ロールしたほうが「あ、数字が変わった」と気づきやすいです。
ある案件で、数字をただ更新していました。ユーザーテストで「フィルター効いてますか?」と聞いたら、半分のユーザーが「わからない」と答えました。数値ロールをつけたら「あ、ちゃんと減った」と全員が気づいてくれました。。
実装方法
HTML構造
HTMLはシンプルです。件数を表示する要素に、idをつけるだけです。
<div class="search-result-header">
<p class="result-count">
検索結果:<span class="result-number" id="js-result-count">128</span>件
</p>
</div>
<div class="filter-bar">
<label>
<input type="checkbox" id="js-filter-pet" /> ペット可
</label>
<label>
<input type="checkbox" id="js-filter-new" /> 新築
</label>
</div>CSSスタイル
数値ロールは、古い数字が上に消えて、新しい数字が下から出てくる動きです。
.result-number {
display: inline-block;
font-size: 24px;
font-weight: 700;
font-variant-numeric: tabular-nums;
min-width: 3ch;
text-align: right;
position: relative;
overflow: hidden;
vertical-align: bottom;
height: 1.2em;
}
.result-number .roll-digit {
display: inline-block;
position: relative;
}
.result-number .roll-digit.is-rolling {
animation: roll-up 0.3s ease-out;
}
@keyframes roll-up {
0% {
transform: translateY(100%);
opacity: 0;
}
60% {
opacity: 1;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
.result-count {
font-size: 14px;
color: #333;
}
@media (prefers-reduced-motion: reduce) {
.result-number .roll-digit.is-rolling {
animation: none;
opacity: 1;
}
}overflow: hiddenで、アニメーション中に数字がはみ出さないようにしています。font-variant-numeric: tabular-numsで数字の幅を揃えて、ロール中にガタつかないようにしています。
JavaScriptの実装
チェックボックスのchangeイベントで件数の変更を受け取り、変わった桁だけロールさせます。
document.addEventListener('DOMContentLoaded', () => {
const countEl = document.getElementById('js-result-count');
if (!countEl) return;
function rollUpdate(el, newValue) {
const oldText = el.textContent;
const newText = String(newValue);
if (oldText === newText) return;
// 桁数を揃える
const maxLen = Math.max(oldText.length, newText.length);
const oldPadded = oldText.padStart(maxLen, ' ');
const newPadded = newText.padStart(maxLen, ' ');
el.textContent = '';
el.style.height = el.offsetHeight + 'px';
for (let i = 0; i < maxLen; i++) {
const span = document.createElement('span');
span.className = 'roll-digit';
span.textContent = newPadded[i];
// 変わった桁だけアニメーション
if (oldPadded[i] !== newPadded[i]) {
span.classList.add('is-rolling');
span.style.animationDelay = (i * 30) + 'ms';
span.addEventListener('animationend', () => {
span.classList.remove('is-rolling');
}, { once: true });
}
el.appendChild(span);
}
// アニメーション完了後にテキストに戻す
setTimeout(() => {
el.textContent = newText;
el.style.height = '';
}, 400);
}
// フィルター変更のシミュレーション
function updateCount(newCount) {
rollUpdate(countEl, newCount);
}
// 実際のプロジェクトではAPIレスポンス後に呼ぶ
const filterPet = document.getElementById('js-filter-pet');
const filterNew = document.getElementById('js-filter-new');
if (filterPet) {
filterPet.addEventListener('change', () => {
const current = parseInt(countEl.textContent, 10);
const next = filterPet.checked
? Math.floor(current * 0.18)
: Math.floor(current / 0.18);
updateCount(next);
});
}
if (filterNew) {
filterNew.addEventListener('change', () => {
const current = parseInt(countEl.textContent, 10);
const next = filterNew.checked
? Math.floor(current * 0.35)
: Math.floor(current / 0.35);
updateCount(next);
});
}
});ポイントは「変わった桁だけロールする」ことです。「128→123」なら、最後の2桁だけがロールします。全部の桁がロールすると、うるさいです。変わった部分だけ動くから、「ここが変わった」と伝わります。
各桁に30msのディレイをつけているので、左から右に波のようにロールします。同時に全桁が動くより自然です。
注意点
実装で気をつけたことです。
アニメーションの時間は0.3秒にしています。0.5秒だと遅くて「待たされている」感じがします。0.15秒だと速すぎて何が起きたかわからない。0.3秒がちょうどいいです。
aria-live="polite"を件数表示の親要素に追加することをおすすめします。スクリーンリーダーのユーザーにも件数の変更が伝わるようになります。今回のサンプルでは省略していますが、実際のプロジェクトでは必ず入れます。
アニメーション完了後にtextContentに戻しています。span要素が残ったままだと、コピー&ペーストしたときに変になるからです。400ms後にシンプルなテキストに戻すことで、DOM構造をきれいに保ちます。
prefers-reduced-motionに対応して、ロールアニメーションを無効にしています。アニメーションなしでも数字は更新されるので、情報は伝わります。
実際の結果
このお客さまのプロジェクトで、A/Bテストを3週間やりました。
| 指標 | 演出なし(A) | 数値ロールあり(B) | 差分 |
|---|---|---|---|
| フィルター変更後の件数認知率 | 47.3% | 78.9% | +31.6pt |
| 「0件」時の条件変更率 | 34.1% | 52.6% | +18.5pt |
| 検索離脱率 | 28.4% | 21.7% | -6.7pt |
件数認知率が31.6ポイント上がりました。一番効果があったのは「0件」のときです。数値ロールで「128→0」と動くと「あ、0件になった」と気づいて、条件を変えてくれるユーザーが18.5ポイント増えました。パッと「0」に変わるだけだと、気づかずにスクロールして「結果がない」と離脱してしまう人が多かったです。ただし、これは1つのプロジェクトでの結果です。効果はサイトの種類やフィルターの数によって変わります。
まとめ
検索結果の件数は「見ればわかる」情報ですが、見てもらえないと意味がないです。フィルターを操作しているユーザーの目はフィルターUIに集中しています。件数表示の変化に気づいてもらうには、「動き」で伝えるのが効果的です。
今回のポイントは以下の3つです:
- 変わった桁だけをロール。全桁ロールはうるさいので、差分だけ動かす
- 各桁に30msのディレイで左から右に波ロール。自然な動きになる
- 0.3秒で完了。
prefers-reduced-motionに対応し、完了後にDOMをクリーンアップ
件数表示のロール演出は、15行ぐらいのCSSと40行ぐらいのJSで実装できます。小さな変更ですが、ユーザーがフィルターの効果を実感できるようになります。
株式会社ファストコーディングでは、こうした検索UIの改善やフロントエンドの実装サポートをしています。「検索結果ページのUXを改善したい」「フィルター操作の手応えを良くしたい」という方は、お問い合わせフォームから気軽にご連絡ください。
※本記事は弊社外国人スタッフによる投稿です。言い回しや表現が不十分な個所がありますことご容赦いただきますようお願いいたします。

