CSSで、アイコンに数字の丸いバッジを付ける方法

​​CSS​HTML,Playwright,アクセシビリティ

通知のアイコンの右上に「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、スマートフォンの実機、実際のスクリーンリーダーでの読み上げは試していません。

薄い水色の角丸の四角に、水色のベルのアイコンが入ったボタンが4つ横に並んでいる。左から1つ目にはバッジがない。2つ目は右上に「3」の丸いピンクのバッジ、3つ目は「12」、4つ目は「99+」で、桁が増えるほどバッジが左へ横長に伸びている。どのバッジも白い縁で囲まれている。各ボタンの下には「空(0件)」「3」「12」「99+」と書いてある
この記事の CSS で付けたバッジ。左から、0件で中身が空のとき、3件、12件、99件より多いとき

HTML は、ボタンの中にバッジの要素を置く

ボタンの中に、アイコンの SVG と、件数を書いたバッジの <span> を入れます。

html
<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 は、ボタンを基準にしてバッジを角に置く

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
3段に分けて、ベルのボタンと「3」「12」「99+」のバッジを並べた画面。1段目の「border-radius: 10px(高さの半分)」では、3は丸、12と99+は両端が丸い横長の形。2段目の「border-radius: 50%」では、3は丸だが、12と99+は楕円になっている。3段目の「box-sizing を付けない(content-box)」では、3も12も横長で、どれも同じくらいの幅になっている
border-radius と box-sizing を変えて、同じバッジを並べたもの

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 で作ります。

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 のピンクの丸が表示されたままでした。

​Posted by 86