dvh とは?
dvh
(dynamic viewport height)は、デバイスのビューポートの高さに基づいて要素の高さを指定できるCSSの単位です。
.section {
height: 100dvh;
}
スマートフォンなど、利用者によって画面サイズが異なる環境でも、それに合わせて自動で高さを調整してくれるため、柔軟なレイアウト設計が可能になります。
便利な反面、意図しない挙動も
スクロールで起こる“かくつき現象”
スマートフォンのモバイルブラウザでは、スクロール操作に応じてアドレスバーが自動的に格納されたり、再展開されたりします。
これによりビューポートの高さが変わり、100dvh
で指定した要素の高さも再計算されます。
その結果、画面全体がガクッと動くように見えてしまうのです。
問題の現象をご覧ください。
左がカクつくページ、右は修正したものです。
再現ページを作りましたので、スマートフォンでアクセスしてみてください。
レイアウト構成
今回の現象は以下のような要件で起こりました。
再現要件の具体例
- ファーストビューに固定ヘッダーがある
- ヘッダー分のスペースを確保するため、下の要素に margin-top を設定している
- ビジュアル画像の高さを calc(100dvh - ヘッダーの高さ) で調整している
これにより、100dvh
で指定した要素が再計算されてしまい、ページ全体がガクッと動いてしまい、ユーザビリティを大きく損なう原因となってしまいます。
コード
実際のコードを見てみましょう。
修正前(カクつく方)は 48px(mt-12)
のマージンを上部に持ち、ヘッダー分下げています。
それだけだと画像がファストビューから下にはみ出てしまうので、 48px
を 100dvh
から引くことで、ぴったりデバイスの高さに収まるということになります。
<main>
<div className="h-[calc(100dvh-48px)] mt-12">
<Image src={photo} alt="" priority className="h-full w-full object-cover" />
</div>
</main>
しかし、アドレスバーが格納・展開するたびに calc
の計算が走り、画像を画面内に収めようと伸びたり縮んだりすることでカクつきが発生してしまいます。
結論: svh を使う
コメントをいただきました。 svh
を使えと……
svh
はスモールビューポート(アドレスバー以外の画面部分)しか見ないので、アドレスバーがあろうがなかろうが高さは変わらない。よって、カクつきは起きないのでした。
ここから下は読まなくても大丈夫です。
そのまま dvh を使って処理する場合
どうしても dvh
を使わなければいけないとき
……があるかはわかりませんが、一応やり方として残しておきます。
初期状態を保存して再利用する
ページに初めてアクセスした段階で、画像の高さは計算されていることに着目します。
この状態を useEffect
で初回レンダリング時のみ計算し、 useState
で管理することで2回目以降の計算が行われずカクつきを防止することができます。
// カスタムフックを作成してロジックを分離
function useInitialViewHeight(): string {
// 初期値も計算済みの値を使用してちらつきを防止(初期レンダリングで使用)
const [viewHeight, setViewHeight] = useState<string>("calc(100dvh - 48px)");
useEffect(() => {
// 初回レンダリング時にのみ高さを計算
// CSS変数の100dvhはモバイルでスクロール時に変動することがあるため
// window.innerHeightで固定値をピクセル単位で取得して再計算を防止
const height = window.innerHeight;
setViewHeight(`${height}px`);
// 空の依存配列で初回レンダリング時のみ実行
}, []);
return viewHeight;
}
export default function AfterPage() {
const viewHeight = useInitialViewHeight();
return (
<main>
<div className="mt-12" style={{ height: `calc(${viewHeight} - 48px)` }} >
<Image src={photo} alt="" priority className="h-full w-full object-cover" />
</div>
</main>
);
}
useEffect
内で同じ計算を行っているのに、初期値にも calc(100dvh - 48px)
を指定する必要があるのかと疑問に思う方もいるかもしれません。
useEffect
が発火する前に、あらかじめ calc(100dvh - 48px)
を初期値として指定しておくことで、初期レンダリング時のちらつきを防止でき、結果としてユーザビリティの向上につながります。