offset-path: ray() で円形メニューの項目を放射状に並べる(sibling-index() と linear() でバネ付きに)
各項目に offset-path: ray(角度) を付けて offset-distance を 0 から 100% に変えると、中心から飛び出す円形(ラジアル)メニューが座標計算なしで作れる。
どんな時に使う?
フローティングボタンを押すと周りにアイコンが広がるメニューや、画面の角から扇形に開くメニューを作りたいとき。三角関数で top と left を計算する代わりに、「角度」と「中心からの距離」で位置を決められる。項目数が変わっても sibling-index() と sibling-count() で角度が自動で割り振られる。
コード
css
.radial {
position: relative;
width: 240px;
aspect-ratio: 1;
}
.radial > .item {
position: absolute;
/* 何番目かで角度を決め、中心から伸びる線の上に置く */
offset-path: ray(calc(360deg / sibling-count() * sibling-index()) closest-side at center);
offset-rotate: 0deg; /* 既定の auto だと線の向きに回転してしまう */
offset-distance: 0%; /* 閉じているときは中心に重ねる */
transition: offset-distance 0.6s
linear(0, 0.6 15%, 1.1 30%, 0.97 45%, 1.01 60%, 1); /* はね返るバネ */
transition-delay: calc(sibling-index() * 40ms); /* 1個ずつずらす */
}
.radial.is-open > .item {
offset-distance: 100%; /* closest-side(中心から一番近い辺まで)だけ外へ */
}ray()の角度は 0deg が真上で、時計回りに増える。closest-sideは線の長さを「起点から一番近い辺まで」にする指定。offset-distance: 100%でその端に着く。px でoffset-distance: 90pxのように半径を直接書いてもよい。- 画面の角から扇形に出したいときは、起点を角にして角度を 90° の範囲に収める。例:
ray(calc(90deg / (sibling-count() - 1) * (sibling-index() - 1)) at left bottom)で真上から真右まで。 - バネの
linear()の作り方は linear() でバネ(スプリング)イージングを CSS だけで書く、ずらし方は sibling-index() で nth-child 地獄なしのスタッガーアニメーション を参照。
注意点・対応状況
offset-path: ray()は Chrome 116、Firefox 122、Safari 16 から対応(Interop 2024 対象で、2024-01 から全主要ブラウザ)。at <位置>の指定は Safari 17.2 から。- 足を引っ張るのは
sibling-index()/sibling-count()の方。未対応ブラウザも考えるなら、style="--i: 0"のように番号をカスタムプロパティで渡す書き方に切り替える。 offset-*はtransformと同じく見た目だけを動かすので、クリック範囲も一緒に動くが、レイアウト上の場所は元のまま。閉じているときはpointer-events: noneやinertで重なった項目を押せないようにする。prefers-reduced-motion: reduceのときはバネと遅延を外すのが無難。
出典
- @gkurttech(2026-11-05、デモ動画。コードは上記で組み立て直したもの)
- 扇形への応用: @gkurttech のリプライ
- 参考: MDN ray()