はじめに
スムーススクロールの実装は未だに悩みます。
ほぼ必須で行う実装ではあるものの、毎回下記のような課題がちらつきます。
- パフォーマンスは落としたくない
- スクロール後にフォーカスも移動させたい(アクセシビリティ対応)
- ページ内検索ではアニメーションさせたくない(ユーザビリティ向上)
- 「視差効果を減らす」がオンのときはアニメーションさせたくない
- イージングが気持ちよかったらよりよい
- 実装は簡単なほうがいい
実装もややこしいし、検証も大変だし、自分なりの慣例が欲しくなったので調べました。
先に結論:今のところよさげな方法
「 html
要素に scroll-behavior: smooth;
+ リンク先に tabindex="-1”
をつける」
2023年12月時点で、個人的にはこの方法が好みでした。
実装はかなりシンプル。
See the Pen Untitled by 伏屋楓 (@eizarkuj-the-reactor) on CodePen.
ここからは、なぜこの方法がよさげだと判断したかの調査メモを書きます。
なぜよさげだと判断したか
まず、スムーススクロールの実装方法をいくつか検討してみます。
- JSライブラリを入れる
- CSS の
scroll-behavior: smooth;
を使う - 独自で実装する
よわよわエンジニアとしては、最後の選択肢はあまり取りたくありません。
(実装方法が様々すぎて、結局何がいいのか、課題をすべて解決できるのかわからないため)
JSライブラリも、jQuery全盛期のころはよく使っていましたが、最近は独自実装に押されている印象です。
となると、CSS の scroll-behavior: smooth;
が気になります。
scroll-behavior
とは
- スクロールが発生した際の振る舞いを設定するプロパティ
- ユーザーが実行したスクロールには影響しない
- このプロパティをルート要素(=
html
要素)に指定した場合、ビューポートに適用される
scroll-behavior
に smooth
を指定することで、スクロールが滑らかになります。名前の通りです。
イージングや秒数は、ユーザーエージェントの定義に従います。
html {
scroll-behavior: smooth; /* 1行だけでスムーススクロールできる! */
}
scroll-behavior
のいいところは、とにかく簡潔なところ。
CSSを1行足すだけなので、何十行もJSを書くよりお手軽にアニメーションをつけられます。
とはいえ、そう簡単には済まないのがWebサイト実装です。
scroll-behavior
の懸念点
- ページ内検索までアニメーションしてしまう
- リンク先に
tabindex
属性をつけないといけない - 「視差効果がを減らす」がオンでも動いてしまう
メインは上記3点でしょうか。
対策次第で解決できるところもあるので、順番に考えます。
懸念点を解決する
ページ内検索までアニメーションしてしまう
ページ内検索を使うときは、目的の場所にすぐ移動したいですよね。
しかし scroll-behavior: smooth;
を設定すると、ページ内検索まで滑らかにスクロールしてしまいます。
検索に時間がかかって不便です。
これは :focus-within
という擬似クラスを使えば解決できます。
:focus-within
は「その要素または子孫要素にフォーカスが当たっている場合」にCSSを適用できる擬似クラスです。
使用例はMDNの下記がわかりやすいです。
このセレクターは、よくある例のように、
<input>
欄の一つにユーザーがフォーカスを置いたときに、それを含む<form>
全体を強調する場合に便利です。
html:focus-within { /* ここを追加 */
scroll-behavior: smooth;
}
:focus-within
追加によって、フォーカスが当たっている要素があるときだけ、スクロールが滑らかになります。
しかし、ここで次の懸念点が出ます。
リンク先に tabindex
属性をつけないといけない
:focus-within
を追加するだけだと、Chrome と Firefox ではスムーススクロールが効きません。
理由は、下記のような挙動が影響しています。
期待している挙動:
- クリックしたリンクに一瞬フォーカスが当たる
- その後、リンク先(移動先)の要素にフォーカスが当たる
- 「フォーカスが当たっている要素があるとき」の条件を満たすので、該当のCSSが適用される
実際の挙動:
- クリックしたリンクに一瞬フォーカスが当たる
- その後、リンク先の要素にフォーカスを当てたい
- が、リンク先の要素にフォーカスの機能がない!
- ので、該当のCSSが適用されない
アンカーリンクの場合、見出しやセクションにリンクすることが多いです。
しかし <h2>
や <section>
, <p>
などには、標準ではフォーカスが当たりません。
それらの要素にフォーカスを当てるために、tabindex
属性の追加が必要です。
<h2 id="can-not-focus">フォーカスが当たらない見出し</h2>
<h2 id="can-focus" tabindex="-1">フォーカスが当たる見出し</h2>
tabindex="-1"
なのは、tabキーでのフォーカスは不可にするためです。
-1
に限らず、負の値であればなんでもOKです。
tabキーでのフォーカスを許可する場合は 0
以上の値を設定してください。
もろもろの課題を解決するには、この属性追加は避けられません。
HTMLに無駄な属性を増やしたくない派には、JSでの独自実装をおすすめします。
「視差効果を減らす」がオンでも動いてしまう
さまざまな事情で、デバイスのアニメーションを減らしたい人もいます。
Apple製品のアクセシビリティ機能のひとつである「視差効果を減らす」が有名ですが、WindowsやAndroidにも同様の設定が存在します。
ここまでの実装では、視差効果を減らす設定がオンでも動いてしまいます。
WCAG (Web Content Accessibility Guidelines) にも下記の達成基準があるので、できれば対応しておきたいところ。
達成基準 2.3.3 インタラクションによるアニメーション (レベル AAA): アニメーションが、機能又は伝達されている情報に必要不可欠でない限り、インタラクションによって引き起こされるモーションアニメーションを無効にできる。
prefers-reduced-motion
というメディアクエリを使います。
ユーザー設定に応じて余計な動きを軽減するためのメディアクエリです。
WCAG基準の「ウェブページの機能や情報に必要不可欠なアニメーション」に該当するもの以外は、上記で囲むことで無効化できます。
スムーススクロールは必要不可欠ではないので、無効化できるようにしておきましょう。
@media (prefers-reduced-motion: no-preference) { /* ここを追加 */
html:focus-within {
scroll-behavior: smooth;
}
}
🤔 : 「必要不可欠なアニメーション」とは
WCAG基準では下記のように書かれている。
もし取り除いてしまうと、コンテンツの情報又は機能を根本的に変えてしまい、かつ、適合する他の方法では情報及び機能を実現できない。
具体例をざっと調べたかぎりでは下記が挙がっていた。
- ローディングアニメーション
- 特に、フォームの送信ボタン押下後
- マウスオーバー
- 利用者の動作に応じてコンテンツが変化するもの
こう見ると、「サイトが動作しているか判別するためのアニメーション」は「必要不可欠」に該当するのかなと思った。
逆に、必要不可欠ではない例では下記が挙がっていた。
- 出現する間に特定の位置に移動する、又はサイズが変化する要素(WCAGより引用)
- パララックス(視差効果)
- スクロールによるトランジション効果
- スムーススクロール
- スクロールに応じた要素表示
- ページめくりのアニメーション
- テキストアニメーション
特に注意したほうがよい動きについては下記が挙がっていた。
- 移動、点滅、バウンス効果
- スライドをフェードに変更するなど
- 点滅がてんかん発作を引き起こす可能性があることは有名
- 「ぼけ」や「にじみ」
アニメーションへの忌避感や症状には個人差があるので、一概には言えない。
WCAGには下記の記載があるので、CSSの transform
プロパティに該当するもの以外は、ある程度は許容されているのかもしれない。
要素の見た目のサイズ、形状、又は位置の変化が生じない、色、ぼかし、又は不透明度の変化は、モーションアニメーションには含まれない。
あらためて完成形を見る
これらの実装をまとめると、最初に載せたcodePenになります。
See the Pen Untitled by 伏屋楓 (@eizarkuj-the-reactor) on CodePen.
要点をまとめるとこんな感じ。
-
html
要素にscroll-behavior: smooth;
を付与- 擬似クラス
:focus-within
を指定しておく - メディアクエリ
prefers-reduced-motion: no-preference
で囲っておく(視差効果に配慮)
- 擬似クラス
- アンカーリンク先に
tabindex="-1”
を付与- 必要に応じて
scroll-margin-top
でヘッダー分の高さをずらす(詳細は後述)
- 必要に応じて
シンプルかつ機能的で、まさに「最低限」という印象です。
(最初に挙げた課題のうち「イージングが気持ちよかったらよりよい」は無視していますが、「よりよい」なのでOKとします)
よくある質問
固定ヘッダーがスクロール後にかぶってしまう
CSSの scroll-margin-top
などを指定しましょう。
イージングを調整したい
残念ながら、 scroll-behavior: smooth;
ではイージングを指定できません。
イージングや秒数にこだわる場合は独自実装が必要です。
今回の方法は、なるべくシンプルに目的だけを果たしたい場合の選択肢としてご検討ください。
記事データをAPIから取得していて tabindex
属性が書けない
HTML解析ツール(HTMLパーサー)を挟めば属性を追加できます。
セクションではなく見出し要素に追加するほうが処理が簡単そうです。
例: cheerio の場合
// 本文内の見出しにtabindex属性を設定
const $ = load(blog.content);
$('h2, h3, h4').attr('tabindex', '-1');
blog.content = $.html();
おわりに
想像より奥が深かったので長くなりました。
読んでくださりありがとうございます。
スムーススクロールの実装方法はさまざまだと思うので、よりよい方法がありましたら教えていただけると幸いです。
参考にさせていただいた記事など
-
scroll-behavior: smooth;
について -
:focus-within
について -
prefers-reduced-motion
について - 「必要不可欠なアニメーション」とは