本文へスキップ

技術

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 で「アンカーだけ滑らか・遷移は即座」は書けない、というのが結論でした。htmlauto のままにして、アンカーのクリックだけを受けるリスナーを document に1つ置いています。

そのリスナーは、次のときには何もしません

  • 修飾キー付きのクリックや中クリック(新しいタブで開きたいとき)
  • prefers-reduced-motion: reduce が設定されているとき
  • 飛び先が見つからないとき

飛び先が tabindex を持つときだけ、スクロールとは別にフォーカスも移します。スキップリンクは飛ぶだけでなくフォーカスが本文へ移ることに意味があるためです。ここを忘れると、押しても画面が動くだけで、キーボードの位置は元のままになります。

入場のアニメーションを JS で書かないのと同じ方針で、動きは基本的に CSS に寄せています。ここだけは、CSS で表現できないことが分かったので JS にしました。
Notes 一覧へ戻る