こんにちは。小学生向けのニュースサイト、こどもニュースをつくっています。
このサイトのヘッダは上部に固定されていて、下段のナビゲーション行がすりガラスになっています。スクロールするとその行だけが畳まれます。
作るときに引っかかった点をまとめます。
backdrop-filter は背景が透けていないと効かない
すりガラスの指定自体は 2 行です。
backdropFilter: 'blur(8px) saturate(135%)',
WebkitBackdropFilter: 'blur(8px) saturate(135%)',
blur だけだと、ぼかした結果が眠い色になります。saturate を足して彩度を上げると、下の色が透けている感じが出ます。数値は見ながら決めました。
引っかかったのは、背景色の扱いでした。
bgcolor: 'rgba(255, 255, 255, 0.00001)',
ほぼ透明な白を指定しています。完全に透明(transparent)だと、環境によって backdrop-filter が効かないことがあります。背景のレイヤーが作られないためです。
わずかでも背景色があると、合成のレイヤーができて効きます。見た目には影響しない値を入れておく、という回避です。
-webkit- 付きも書いています。この指定は接頭辞が必要な環境がまだあります。
上段は不透明にする
このサイトのヘッダは 2 段構成です。上段はサイト名を出す色付きのバーで、こちらは不透明にしています。
すりガラスは、下に何かがあるときにきれいに見えます。ページの最上部では、下にあるのはページの地色だけなので、ぼかす対象がありません。
上段は常に見えているので、不透明の色を置いたほうが締まります。下段はスクロールしたときに本文の上に重なるので、そこですりガラスが効きます。
「全部すりガラス」にすると、効く場所と効かない場所が混ざって統一感が出ませんでした。効果が生きる場所だけに使う、という判断です。
畳み方: 高さを 0 にする
スクロールしたときは、下段を畳みます。
minHeight: hidden ? 0 : { xs: …, sm: … },
maxHeight: hidden ? 0 : { xs: …, sm: … },
opacity: hidden ? 0 : 1,
overflow: 'hidden',
visibility: hidden ? 'hidden' : 'visible',
transition: 'max-height 180ms ease, min-height 180ms ease, opacity 140ms ease, visibility 180ms ease',
高さを 0 にして、overflow: hidden で中身を隠します。display: none にすると遷移が効かないので、高さのアニメーションで畳みます。
visibility も切り替えているのは、畳んだ状態でも中身がフォーカスできてしまうのを防ぐためです。高さが 0 でも、要素は存在します。タブ移動で「見えないリンク」にフォーカスが移ると、どこにいるのか分からなくなります。
aria-hidden も同時に付けています。
aria-hidden={hidden}
読み上げソフトからも隠す、という指定です。視覚的に隠すのと、支援技術から隠すのは別の指定になります。
遷移の時間を分ける
transition で、プロパティごとに時間を変えています。
max-height 180ms, min-height 180ms, opacity 140ms, visibility 180ms
透明度を少し速くしています。畳むときは先に薄くなってから縮むので、動きが自然に見えます。
全部を同じ時間にすると、縮みながら薄くなって、最後にかくっと消える感じになります。数十ミリ秒の差ですが、印象が変わりました。
何をもって「隠す」か
スクロールの向きで畳むかどうかを決めますが、素朴に実装すると細かく反応しすぎます。少し下げただけで隠れ、少し上げただけで出てきます。
判定には「折り返した量」を使いました。
/** 切り替えに必要な移動量(px)。畳む対象より大きいこと。 */
export const HIDE_DEADBAND = 72;
/** 先頭付近では常に出す(ここより上では隠さない)。 */
export const HIDE_SHOW_AT_TOP = 80;
ある程度の量を動かして初めて切り替わります。この量は、畳む対象の高さより大きくしています。小さいと、隠れた瞬間にレイアウトが動いて、その動きがまた判定を引き起こす、というループが起きます。
ページ先頭の付近では常に表示します。一番上まで戻ったのにナビが隠れている、という状態を避けるためです。
判定の部分は純粋関数に切り出して、テストしています。
export function nextHideState(input: {
y: number; anchor: number; hidden: boolean;
deadband?: number; showAtTop?: number;
}): { hidden: boolean; anchor: number } { … }
スクロールの状態遷移は、実際に動かして確認するのが難しいものです。入力と出力の関数にしておくと、境界の挙動をテストで固定できます。
スクロールの監視は間引く
scroll イベントは大量に発火します。そのたびに状態を計算すると重くなります。
requestAnimationFrame で間引くフックを作って、ヘッダと目次で共有しています。フレームごとに 1 回だけ計算する形です。
同じ間引きが複数箇所で要るなら、フックにまとめておくと実装が揃います。
まとめ
-
backdrop-filterはblurにsaturateを足すと色が生きる - 完全に透明な背景だと効かないことがある。わずかな背景色を入れて合成レイヤーを作る
- すりガラスは下に何かがある場所でだけ使う。全部に掛けると統一感が出ない
- 畳むときは高さを 0 にする。
visibilityとaria-hiddenも同時に切り替える - 遷移の時間はプロパティごとに変えると自然に見える
- スクロールでの切り替えは、畳む対象より大きい移動量を閾値にする。小さいとループする
- 判定は純粋関数に切り出してテストする