CSSで、アイコンに数字の丸いバッジを付ける方法
通知のアイコンの右上に「3」のような件数のバッジを付けるには、アイコンを包む要素に position: relative を付け、バッジを position: absolute でその角に置きます。バッジは高さを決めて min-width を同じ値にすると、1桁のときは丸に、2桁以上のときは両端の丸い横長の形になります。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS のページを Playwright で開き、getBoundingClientRect() で位置と大きさを、Chrome DevTools Protocol でボタンのアクセシブルな名前を記録しました。Firefox と Safari、スマートフォンの実機、実際のスクリーンリーダーでの読み上げは試していません。

HTML は、ボタンの中にバッジの要素を置く
ボタンの中に、アイコンの SVG と、件数を書いたバッジの <span> を入れます。
<button class="icon-button">
<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true">…</svg>
<span class="visually-hidden">通知</span>
<span class="badge">3<span class="visually-hidden">件</span></span>
</button>
visually-hidden は、画面には出さずに、ボタンのアクセシブルな名前には含める文字に付けるクラスです。このクラスを使う理由は、後の「スクリーンリーダーに件数を伝える」の節で書きます。
CSS は、ボタンを基準にしてバッジを角に置く
.icon-button {
position: relative;
display: inline-grid;
place-items: center;
width: 44px;
height: 44px;
border: 0;
border-radius: 12px;
background: #eefafd;
color: #067a98;
}
.badge {
position: absolute;
top: -6px;
right: -6px;
box-sizing: border-box;
min-width: 20px;
height: 20px;
padding: 0 6px;
border-radius: 10px;
background: #d85878;
color: #fff;
font-size: 12px;
font-weight: 700;
line-height: 20px;
text-align: center;
box-shadow: 0 0 0 2px #fff;
}
.badge:empty {
display: none;
}
position: absolute を付けた要素は、MDN の position のページにあるとおり、static ではない直近の祖先を基準に置かれます。ここではボタンに position: relative を付けて、ボタンを基準にしています。top: -6px と right: -6px で、バッジをボタンの右上の角から少し外へはみ出させました。
検証環境で測ると、3件、12件、99件より多いときのどのバッジも、右端がボタンの右端より 6px 右、上端がボタンの上端より 6px 上にありました。
ボタンに position を付け忘れると、ページの右上に飛ぶ
ボタンの position: relative を消し、static に戻して試しました。幅 600px の画面で、ボタンは左上から (100, 100) の位置にあるのに、バッジは左端が 586px、上端が -6px で、ページの右上の角に出ていました。基準にできる祖先がないため、MDN の説明のとおり、文書のルート要素の包含ブロック(初期包含ブロック)が基準になります。バッジがアイコンから離れたところに出たら、まずアイコンを包む要素の position を確かめます。
1桁は丸に、2桁以上は横長にする
桁が増えたときに、両端の丸い形のまま横に伸ばすには、高さと min-width、border-radius、right の指定を組み合わせます。
height と min-width を同じ 20px にすると、1桁のときは正方形になります。文字が増えて幅が足りなくなると、min-width を超えて横に広がります。border-radius を高さの半分の 10px にすると、正方形のときは丸に、横長のときは両端が半円の形になります。right で右端を固定しているので、バッジは左へ伸びます。
検証環境で測ったバッジの大きさは、次のとおりでした。
| 件数 | バッジの幅 | バッジの高さ |
|---|---|---|
| 3 | 20px | 20px |
| 12 | 25.36px | 20px |
| 99+ | 32.36px | 20px |

border-radius: 50% にすると楕円になる
border-radius: 50% にすると、1桁のバッジは丸になりますが、2桁以上では楕円になりました。MDN の border-radius のページにあるとおり、パーセントの値は、横の半径がボックスの幅に、縦の半径がボックスの高さに対する割合です。幅 25.36px・高さ 20px の「12」のバッジでは、角の横の半径が 12.68px、縦の半径が 10px になり、両端が半円になりません(12.68px は、幅 25.36px の 50% です)。高さが決まっているバッジでは、半径を高さの半分の px で書きます。
box-sizing: border-box を付けないと、1桁でも丸にならない
box-sizing: border-box を外すと、「3」のバッジは幅 32px・高さ 20px の横長になりました。既定の content-box では、min-width: 20px は文字の入る部分の幅で、そこに左右の padding 6px ずつが足されるためです。border-box にすると、MDN の box-sizing のページにあるとおり、幅と高さに padding と境界も含まれます。min-width: 20px が、バッジ全体の最小の幅になります。
padding: 0 6px は、2桁以上のときに数字の左右に余白を作るためのものです。1桁のときは、文字の幅に padding を足しても 20px に届かず、min-width の 20px が使われます。
白い縁は box-shadow で付ける
バッジのまわりの白い縁は、ぼかしのない box-shadow: 0 0 0 2px #fff で描いています。4つ目の値は影を広げる大きさで、MDN の box-shadow のページにあるとおり、正の値で影が外側へ広がります。白い縁があると、バッジがアイコンや背景の色に重なっても、境目が見えます。
影なら、バッジの大きさは変わりません。検証環境で、「3」のバッジの getBoundingClientRect() の幅は、白い縁があってもなくても 20px でした。同じ縁を border: 2px solid #fff で付けると、border-box では文字の幅と左右の padding 12px に、境界の 4px が加わって 20px を超えるため、「3」のバッジは幅 22.69px・高さ 20px になり、丸ではなくなりました。「12」のバッジも、25.36px から 29.36px に広がりました。
0件のときは、中身を空にして隠す
.badge:empty { display: none; } は、中身が何もないバッジを隠します。0件のときにバッジの中身を空で出力すると、CSS だけでバッジが消えます。最初の画像のいちばん左のボタンがこの状態で、検証環境では display が none になっていました。
MDN の :empty のページにあるとおり、:empty では、空白文字を含む文字列も子として数えます。バッジの中身を半角スペース1つにして試すと、:empty に当てはまらず、display は block のままでした。テンプレートで出力するときは、タグの間に改行や空白を入れないようにします。
:empty で隠せるのは、中身が本当に空のときだけです。検証環境で「0」という文字を入れたバッジは、:empty に当てはまらず、20px の丸として表示されたままでした。
スクリーンリーダーに件数を伝える
MDN の aria-label のページにあるとおり、ボタンのアクセシブルな名前は、既定ではボタンの中身から作られます。アイコンの SVG とバッジだけを入れたボタンでは、バッジの数字だけが中身になり、検証環境ではボタンの名前が「3」になりました。これでは、何が3なのか分かりません。
ボタンに aria-label="通知" を付けると、名前は「通知」になりましたが、件数の「3」は名前から消えました。aria-label で決めた名前が、中身から作られる名前の代わりに使われたためです。
この記事の HTML のように、画面に出さない文字で「通知」と「件」を足すと、ボタンの名前は「通知 3 件」になりました。アイコンの SVG には aria-hidden="true" を付けています。MDN の aria-hidden のページにあるとおり、この属性を付けた要素は、子要素も含めてアクセシビリティツリーから除かれます。画面に出さない文字は、次の CSS で作ります。
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
「件」をバッジの中に入れているので、0件で中身を空にするときは「件」も一緒に消します。検証環境で、中身が <span class="visually-hidden">件</span> だけのバッジは :empty に当てはまらず、数字のない 20px のピンクの丸が表示されたままでした。





