CSSで、パンくずリストを矢印の形で並べる方法

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

パンくずリストの各リンクを 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、スマートフォンの実機、実際のスクリーンリーダー、右から左に書く言語での表示は試していません。

4つの項目が矢印の形でつながったパンくずリスト。「ホーム」「猫の手帳」「毛づくろい」は薄い水色、最後の「ブラッシングの道具」は濃い水色に白い文字。矢印と矢印の間に細い白いすき間がある
この記事の CSS を Chromium で表示したもの(上の文字は画像用に足したもの)

HTML は nav と ol で書く

HTML は、矢印にしない場合と同じです。

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 にまとめました。

css
.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つの頂点は、左上から時計回りに次の位置です。

  1. 左上(0 0)
  2. 上の辺の、右端から --tip だけ手前(calc(100% - var(--tip)) 0)
  3. 右端の高さの真ん中。ここが矢印の先になる(100% 50%)
  4. 下の辺の、右端から --tip だけ手前
  5. 左下(0 100%)
  6. 左端から --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 のアクセシビリティツリーを書き出した結果です。

text
- 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 に枠を描きます。

css
.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つのパンくずリスト。どちらも「ホーム」にフォーカスがある。上の「a に outline を付けたもの」は、どの項目にも枠が見えない。下の「親の li に drop-shadow を付けたもの」は、「ホーム」の矢印の形に沿ってピンクの枠が付いている
Tab キーで「ホーム」にフォーカスしたところ(上の文字は画像用に足したもの)

折り返したときに、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行目の先頭の項目は、左のくぼみが付いたままになります。前の行から続いていることが分かる形なので、この記事ではそのままにしています。

幅 360px で折り返したパンくずリストを上下に2つ並べた画像。どちらもリストの左端にピンクの点線がある。上の「2つ目以降の li だけを左にずらしたもの」は、2行目の「ブラッシングの道具」が点線より左にはみ出している。下の「ol に左の余白を付け、すべての li をずらしたもの」は、2行目の左端が点線にそろっている
幅 360px で2行に折り返したところ。ピンクの点線はリストの左端(点線と上の文字は画像用に足したもの)

​Posted by 86