技術
scroll-behavior: smooth で3回間違えた話
「ページ内のリンクだけ滑らかに送りたい。ページの移動は今までどおり即座に」。これだけのことに3回間違えました。結論から言うと、CSS では表現できません。
1回目 —— html に素で当てる
いちばん普通のやり方から始めました。
html { scroll-behavior: smooth }アンカーは滑らかになりました。代わりにページを移動したときのスクロール位置のリセットまで滑らかになりました。このサイトはページ遷移に View Transitions API を使っているので、そちらと競合して滑らかな移動が完走せず、前のページの位置が新しいページの最大値に丸められて残ります。
長い法務ページの下部からサポートへ移ると、11,014px から 1,314px に着きました。本来は 0px、つまりページの先頭です。
2回目 —— リンクにフォーカスがあるときだけにする
アンカーを押した瞬間だけ滑らかにすればいい。そう考えて条件を付けました。
html:focus-within { scroll-behavior: smooth }これは条件として成立しません。実際にクリックして、イベントごとに計算値を測るとこうなります。
mousedown focusWithin=false behavior=auto active=BODY
focus focusWithin=true behavior=smooth active=A
click(既定動作の直前) focusWithin=true behavior=smooth active=A
click+0ms focusWithin=false behavior=auto active=BODY ←クリックの直後にフォーカスがリンクから外れ、値は auto に戻ります。スクロールが走るのは、その後です。 飛び先の要素はフォーカスを受け取れないので、ブラウザがフォーカスを body へ返すためです。
書いた条件はまちがいなく発火しているのに、肝心の瞬間には当たっていない。読んでも分からず、測って初めて分かった類の間違いでした。
3回目 —— 着いた先を光らせようとする
ついでに「飛んだ先の節の左に罫を引く」演出を入れようとして、これも失敗しました。
:target { animation: ... }:target が当たるのは節の器です。罫の長さが器の高さになりました。特商法の節で 1,571px、「ページ上部へ」の飛び先ではページ本体まるごとの 11,689px。スクロール中に画面いっぱいの縦線が走って見えます。
着地したことは、滑らかに送られること自体で十分に分かります。演出は足しませんでした。
いまの形 —— アンカーだけを JS で受ける
CSS で「アンカーだけ滑らか・遷移は即座」は書けない、というのが結論でした。html は auto のままにして、アンカーのクリックだけを受けるリスナーを document に1つ置いています。
そのリスナーは、次のときには何もしません。
- 修飾キー付きのクリックや中クリック(新しいタブで開きたいとき)
prefers-reduced-motion: reduceが設定されているとき- 飛び先が見つからないとき
飛び先が tabindex を持つときだけ、スクロールとは別にフォーカスも移します。スキップリンクは飛ぶだけでなくフォーカスが本文へ移ることに意味があるためです。ここを忘れると、押しても画面が動くだけで、キーボードの位置は元のままになります。