CSSに以下の“おまじない”を書いておくと、コンテンツが画面幅をはみ出していた場合でも横スクロールが発生しないようにできますが、そもそもなぜhtmlとbodyの両方に"overflow-x: hidden"を設定する必要があるのでしょうか?
html, body {
overflow-x: hidden;
}
このコードを理解するカギは、CSSのOverflow Viewport Propagationという仕組みです。詳細は後述するとして、まずはoverflow
プロパティの値をvisible
やhidden
に変えてhtml要素とbody要素に適用した場合に、境界をはみ出したコンテンツをブラウザーがどのように表示するかを確認してみましょう。
html {
overflow: visible; /* 初期値 */
}
body {
overflow: hidden;
}
サンプルコードはこちら:
https://codepen.io/kaz_hashimoto/pen/jOKQQQx
visible
とhidden
の組み合わせ4通りについてデスクトップ版Chromeで表示すると、結果は図1に示した画面A〜Dのようになりました。
図1
赤い境界線のボックスは<html>要素、青い境界線は<body>要素です。ビューポートとの境界を見分けられるように、<html>と<body>の寸法はウインドウよりも小さくしてあります。
図1の結果を要約すると
画面 | <html> | <body> | scroll | はみ出し | はみ出し方 |
---|---|---|---|---|---|
A | visible | visible | Yes | Yes | window < content |
B | visible | hidden | No | Yes | html < content ≤ window |
C | hidden | visible | No | Yes | html < content ≤ window |
D | hidden | hidden | No | No | content ≤ body |
※「はみ出し方」の記号の意味は直感的ではありますが、
<:境界で content がクリップされない、≤:クリップされる。
画面B, Cではoverflow: hidden
が効いてないように見えますね。CSSに指定したはずのhidden
の値はどこに行ってしまったのでしょうか?
ここにOverflow Viewport Propagationの仕組みが働いているのです(図2)。
Overflow Viewport Propagationとは
CSS Overflow Module Level 3の3.5. Overflow Viewport Propagationには次のように書かれています。わかりやすくするため、1つのパラグラフを3つの文に分けて説明します。
仕様1
UAs must apply the 'overflow-*' values set on the root element to the viewport when the root element’s 'display' value is not 'none'.
(DeepL翻訳)
UA は、ルート要素の 'display' の値が 'none' でない場合、ルート要素に設定された 'overflow-*' 値をビューポートに適用しなければならない。
たとえば画面Cの場合、<html>に設定したhidden
値がビューポートに適用されます(図3a)。
仕様2
However, when the root element is an [HTML] <html> element (including XML syntax for HTML) whose 'overflow' value is 'visible' (in both axes), and that element has as a child a <body> element whose 'display' value is also not 'none', user agents must instead apply the 'overflow-*' values of the first such child element to the viewport.
(DeepL翻訳)
しかし、ルート要素が[HTML]の<html>要素(HTMLのXML構文を含む)であり、その'overflow'の値が(両軸とも)'visible'で、その要素が子として<body>要素を持っている場合、ユーザーエージェントは代わりにその最初の子要素の 'overflow-*' 値をビューポートに適用しなければならない。
たとえば画面Bの場合、<html>に設定したoverflow
の値がvisible
なので、<body>に設定したhidden
値がビューポートに適用されます(図3bの左)。
仕様3
The element from which the value is propagated must then have a used 'overflow' value of 'visible'.
(意訳)
値の伝達元の要素はその後、'overflow'の使用値が'visible'でなければならない。
たとえば画面Bの場合、<body>に設定したhidden
値がビューポートに伝達されたので、伝達元の<body>側が使用値としてvisible
になります1。
画面B, Cでoverflow: hidden
が効いてないように見える理由もこれでわかりました。つまり、
- <html>と<body>に設定していた
overflow
の値が両方とも使用値としてvisible
に変わるため(図3c)、コンテントのテキストが境界からはみ出してしまう。 - ビューポート側には
overflow
の値にhidden
が適用されるため、<html>の境界からもはみ出したテキストは画面の端でクリッピングされて、画面はスクロールしない。
参考)仕様の解釈はこちらの回答も参考にしました。
stackoverflow#14718319: Why does overflow-x: hidden make my absolutely positioned element become fixed?
スマホでは状況が違う
画面B, Cでスクロールしないのなら、冒頭の問いに戻って、
横スクロールを防止するために
なぜhtmlとbodyの両方に"overflow-x: hidden"を設定する必要があるのでしょうか?
そこで、画面Bと同じページをスマホで表示してみましょう。iOSとAndroidのいくつかのバージョンで試した結果は図4のとおり。画面がスクロールしてしまうものがあります。
図4
※使用したブラウザは、Safari (iOS), Chrome (Android)です。
iOSの場合、15.2はスクロールが発生しますが、同じv.15系でも15.5はスクロールが発生しません。一方、Androidはv10, 11, 12すべてスクロールが発生しました2。
これらの事実を踏まえると、デスクトップでもスマホでも確実に横スクロールを防止できるパターンは画面Dしか残されていません。すなわち、htmlとbodyの両方に"overflow-x: hidden"を設定する必要があるわけですね。やっと理解できました。
補足
ちなみに、CSS仕様書の3.5. Overflow Viewport Propagationに、スマホでの表示に関する注記があります。しかし、この注記が導入3されるきっかけとなったissue4は図4に示した現象とは異なるため、この現象との関連性はわかりませんでした。
Note: 'overflow: hidden' on the root element might not clip everything outside the Initial Containing Block if the ICB is smaller than the viewport, which can happen on mobile.
(DeepL翻訳)
注:ルート要素の 'overflow: hidden' は、初期包含ブロック(ICB)がビューポートより小さい場合5、ICBの外側のすべてをクリップしないことがあります(モバイルで発生することがあります)。
-
overflow
プロパティに関して、Window.getComputedStyle()が返す値やChrome DevToolsのComputedタブには計算値が表示されるため、これらの方法では使用値の変化を確認できません。 ↩ -
さらにiOSの場合と異なり、ページ上部に
position: fixed
で配置してある黒いボックスも一緒に横へスクロールしてしまいます。 ↩ -
Working Draft, 2 December 2021 ↩
-
[css-overflow] Clarify what rect clips the root element with overflow:hidden on mobile environments with dynamic toolbar #5646 ↩
-
定義によると、初期包含ブロックとはルート要素(html)の包含ブロックだから、CSS2.1§10.1の定義によると"it has the dimensions of the viewport"とあるので、「ICBがビューポートより小さい場合」の意味がよくわかりませんでした。 ↩