CSSで、パンくずリストを矢印の形で並べる方法
パンくずリストの各リンクを clip-path の polygon() で、右がとがり左がへこんだ矢印の形に切り抜きます。そのうえで各項目を左にずらし、前の矢印の先を次の項目のくぼみに入れると、矢印がつながって並びます。区切りを形で見せるので、「>」のような区切りの文字は HTML にも CSS の content にも入れません。
ただし、切り抜いただけでは、フォーカスの枠が見えなくなり、2行に折り返したときには2行目が左にはみ出します。どちらも CSS を少し足せば直せます。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開き、要素の位置、elementFromPoint() の結果、スクリーンショットの画素の色を読んで確かめました。Firefox と Safari、スマートフォンの実機、実際のスクリーンリーダー、右から左に書く言語での表示は試していません。

HTML は nav と ol で書く
HTML は、矢印にしない場合と同じです。
<nav aria-label="パンくずリスト">
<ol class="breadcrumb">
<li><a href="/">ホーム</a></li>
<li><a href="/notes/">猫の手帳</a></li>
<li><a href="/notes/grooming/">毛づくろい</a></li>
<li><a href="/notes/grooming/brush/" aria-current="page">ブラッシングの道具</a></li>
</ol>
</nav>
WAI-ARIA Authoring Practices の Breadcrumb Pattern では、パンくずリストを aria-label などで名前を付けた navigation ランドマークの中に置き、現在のページへのリンクに aria-current="page" を付けるとしています。MDN の aria-current のページにも、見た目で現在のページを区別しているなら、そのリンクに aria-current="page" を付ける必要があると書かれています。この記事では、現在のページの項目を濃い色にして区別しています。
リンクを clip-path で矢印の形に切り抜く
各項目の a を、clip-path: polygon() で切り抜きます。矢印の先の長さは --tip、矢印と矢印のすき間は --gap にまとめました。
.breadcrumb {
--tip: 14px; /* 矢印の先の長さ */
--gap: 3px; /* 矢印と矢印のすき間 */
display: flex;
flex-wrap: wrap;
row-gap: 6px;
margin: 0;
padding: 0 0 0 calc(var(--tip) - var(--gap)); /* 1つ目の項目の左へのずれを打ち消す */
list-style: none;
}
.breadcrumb li {
margin-left: calc(var(--gap) - var(--tip)); /* 前の項目の先を、くぼみに入れる */
}
.breadcrumb a {
display: block;
padding: 8px 16px 8px calc(var(--tip) + 10px);
background: #d6f5fb;
color: #067a98;
text-decoration: none;
clip-path: polygon(
0 0,
calc(100% - var(--tip)) 0,
100% 50%,
calc(100% - var(--tip)) 100%,
0 100%,
var(--tip) 50%
);
}
.breadcrumb li:first-child a {
padding-left: 14px;
clip-path: polygon(0 0, calc(100% - var(--tip)) 0, 100% 50%, calc(100% - var(--tip)) 100%, 0 100%);
}
.breadcrumb a:hover { background: #b3ecf7; }
.breadcrumb a[aria-current="page"] { background: #067a98; color: #fff; }
polygon() には、頂点の x 座標と y 座標の組を、カンマで区切って並べます。座標は要素の左上からの位置で、% で書くと要素の幅や高さに対する割合になります。上の6つの頂点は、左上から時計回りに次の位置です。
- 左上(
0 0) - 上の辺の、右端から
--tipだけ手前(calc(100% - var(--tip)) 0) - 右端の高さの真ん中。ここが矢印の先になる(
100% 50%) - 下の辺の、右端から
--tipだけ手前 - 左下(
0 100%) - 左端から
--tipだけ入った、高さの真ん中。ここが左のくぼみになる(var(--tip) 50%)
右の2つの頂点も calc(100% - var(--tip)) で書いているので、文字の長さで項目の幅が変わっても、矢印の先の長さは 14px のままです。1つ目の項目は前に続く項目がなく、くぼみが要らないので、6つ目の頂点を省いた5つの頂点で切り抜いています。左の padding に --tip を足しているのは、文字がくぼみに重ならないようにするためです。
前の矢印の先を、次の項目のくぼみに入れる
切り抜いただけでは、項目と項目の間に、矢印の先とくぼみの分の白い三角が空きます。そこで、各 li に margin-left: calc(var(--gap) - var(--tip))、つまり -11px を付けて左にずらし、前の項目の矢印の先を次の項目のくぼみに入れます。
検証環境で幅 800px のページに表示すると、どの隣り合う2つの項目も、要素の箱が 11px ずつ重なっていました。「ホーム」と「猫の手帳」の境目で、高さの真ん中の行の画素を左から読むと、「ホーム」の水色が x=92 まで続き、x=93〜95 の3つは白か白に近い色で、x=96 から「猫の手帳」の色になりました。上から2行目の画素の並びでも、境目に白い画素がありました。--gap の 3px が、矢印と矢印の間の白いすき間として残っています。
重なった部分をクリックすると、見えている項目に届く
要素の箱は重なっていますが、クリックは見えている形のとおりに届きます。「ホーム」の矢印の先のうち、「猫の手帳」の箱の中に入っている点で document.elementFromPoint() を呼ぶと、返ったのは「ホーム」の a でした。その点を Playwright の mouse.click() でクリックすると、移動しようとしたリンクは「ホーム」の / でした。矢印の先と次のくぼみの間の白いすき間では、elementFromPoint() が返したのは li で、クリックしてもリンクには届きませんでした。
MDN の clip-path のページにあるとおり、clip-path は切り抜いた形の外側を表示しません。検証では、表示されない部分はクリックも受け取らず、その下にある前の項目にクリックが届きました。
区切りの文字を入れないので、読み上げに混ざらない
Playwright の ariaSnapshot() で、nav のアクセシビリティツリーを書き出した結果です。
- navigation "パンくずリスト":
- list:
- listitem:
- link "ホーム":
- /url: /
- listitem:
- link "猫の手帳":
- /url: /notes/
- listitem:
- link "毛づくろい":
- /url: /notes/grooming/
- listitem:
- link "ブラッシングの道具":
- /url: /notes/grooming/brush/
並んでいるのはリンクの名前だけで、区切りに当たるものは出てきません。MDN のパンくずナビゲーションのページでは、CSS の content で足した区切りの矢印「→」は、スクリーンリーダーなどの支援技術に公開されると注意しています。矢印を形で描けば、この心配はありません。
フォーカスの枠は、親の li に drop-shadow で描く
clip-path は、要素の outline もいっしょに切り取ります。Tab キーで「ホーム」にフォーカスを移し、フォーカスの前と後のスクリーンショットを比べました。
| フォーカスの枠の付け方 | フォーカスの前後で変わった画素 |
|---|---|
ブラウザーの既定のまま(outline-style: auto) |
6 個 |
a に outline: 2px solid #d85878; outline-offset: 2px |
0 個 |
既定の枠では、変わった画素は6個だけでした。outline を自分で付けた場合は、1画素も変わりませんでした。MDN の outline のページにあるとおり、outline は境界線の外側に描かれます。clip-path の座標の基準になる箱(参照ボックス)は、指定しなければ境界線の外側の端まで(border-box)なので、outline はすべて形の外側になり、切り取られます。これでは、キーボードで操作する人が、どの項目にフォーカスがあるのか分かりません。
そこで、a の outline は消し、切り抜かれていない親の li に枠を描きます。
.breadcrumb a:focus-visible { outline: none; }
.breadcrumb li:has(> a:focus-visible) {
filter:
drop-shadow(2px 0 0 #d85878)
drop-shadow(-2px 0 0 #d85878)
drop-shadow(0 2px 0 #d85878)
drop-shadow(0 -2px 0 #d85878);
}
drop-shadow() は、box-shadow と違って、ボックスの四角ではなく、表示されている形(アルファチャネル)に沿った影を作ります。li の中で見えているのは矢印の形に切り抜かれた a だけなので、影も矢印の形になります。drop-shadow() には影を広げる spread を指定できないため、ぼかしなしの影を上下左右に 2px ずつずらして4つ重ね、枠のように見せています。:focus-visible は、フォーカスを目に見える形で示すべきだとブラウザーが判断したときに当てはまる擬似クラスで、キーボードで操作しているときには当てはまります。:has() と組み合わせた li:has(> a:focus-visible) で、その状態の a を子に持つ li を選んでいます。
この CSS で「ホーム」にフォーカスすると、ピンクの画素が 444 個になり、横の範囲は x=14〜94 でした。「ホーム」の箱は x=16〜93.5 なので、枠は左に 2px ほど外側まで出ています。2つ目の「猫の手帳」にフォーカスした場合も、左のくぼみの奥と、右の矢印の先の外側にピンクの画素がありました。枠はくぼみと矢印の先に沿って描かれています。

折り返したときに、2行目が左にはみ出さないようにする
項目が多い場合や画面が狭い場合は、flex-wrap: wrap で2行目に折り返します。このとき、ずらし方によっては2行目の先頭が左にはみ出します。
比べるために、li + li で2つ目以降の項目だけを左にずらす書き方も試しました。この書き方では、2行目の先頭になった項目も2つ目以降の項目なので、左にずれたままになります。検証環境で幅 360px にすると、「ブラッシングの道具」が2行目に折り返し、その左端は x=5 でした。リストの左端は x=16 なので、11px はみ出しています。
この記事の CSS では、1つ目を含むすべての li を同じだけ左にずらし、そのずれを ol の左の padding で打ち消しています。こうすると、どの行でも先頭の項目の左端がリストの左端にそろいます。同じ幅 360px で、2行目の「ブラッシングの道具」の左端は、1行目の「ホーム」と同じ x=16 でした。2行目の先頭の項目は、左のくぼみが付いたままになります。前の行から続いていることが分かる形なので、この記事ではそのままにしています。






