ぬにょす(挨拶)。
Webの画面でヘッダ・フッタを固定表示しつつ、メインコンテンツの高さをいっぱいに広げたい場合、FlexBoxと100vh指定で万事OKかと思っていたら、そうでも無かったので解決策を載せます。
なお、自分の環境上React + Chakra UIでの説明になります。他の環境の場合は、適宜読み替えてください。
実現したいこと
こんなイメージです。
スマホ・タブレットへの対応 その1
PCブラウザであれば、以下のようなコードで簡単に実現できます。
<Flex direction="column" height="100vh">
<Box bgColor="blue.500">Header</Box>
<Box flexGrow="1" bgColor="green.500">
Main
</Box>
<Box bgColor="yellow.500">Footer</Box>
</Flex>
Code language: HTML, XML (xml)
しかしスマホやタブレットのブラウザでは、アドレスバーやナビゲーションボタンでコンテンツが隠れてしまいます。レスポンシブな見た目をデベロッパーツールだけで確認してると気づかないですね。
React向けに react-div-100vh というパッケージがあるので、こちらを利用して
<Div100vh>
<Flex direction="column" height="100%">
<Box bgColor="blue.500">Header</Box>
<Box flexGrow="1" bgColor="green.500">
Main
</Box>
<Box bgColor="yellow.500">Footer</Box>
</Flex>
</Div100vh>
Code language: HTML, XML (xml)
<Div100vh>
コンポーネントでラップすれば、このような表示になります。
スマホ・タブレットへの対応 その2
よしよしと思ったのですが、画面を縦→横→縦のように回転させると、このようになってしまいました。縦画面に戻したとき、横画面のときの高さのままになっています。
デベロッパーツールで画面を回転させても、このようなことは起こりません。また、スマホ・タブレットでも高さがちゃんと戻る場合もあります。しかし「こうなることがある」と分かった以上は対応したいのが人情というかプライドと申しましょうか。
調べてみたところ、window.innerHeightの更新タイミングにタイムラグがあるらしいと分かりました。
そこで、画面の回転をCSSメディアクエリで検知しつつ、タイマーでinnerHeightを再取得するようにしました。
const trigger = use100vh();
const [height, setHeight] = useState<number | null>();
const [isLandscape] = useMediaQuery("(orientation: landscape)");
useEffect(() => {
const timerId = setTimeout(() => {
setHeight(measureHeight());
}, 100);
return () => {
clearTimeout(timerId);
};
}, [isLandscape, trigger]);
return (
<Flex direction="column" height={height ? `${height}px` : "100vh"}>
<Box bgColor="blue.500">Header</Box>
<Box flexGrow="1" bgColor="green.500">
Main
</Box>
<Box bgColor="yellow.500">Footer</Box>
</Flex>
);
Code language: JavaScript (javascript)
use100vh()
と measureHeight()
は react-div-100vh が提供する機能です。useMediaQuery()
は Chakra-UI が提供する機能です。
ちなみにタイマーの時間が小さすぎる(10ミリ秒とか)と、うまくいかないことがありました。
まとめ
- スマホ・タブレットでは単に100vh指定してもダメ。
- リサイズ等のイベント発火時、window.innerHeightが更新されていないことがある。
コメント