html, body { overflow-x: hidden; } 両方に設定するのはなぜ？

CSSに以下の“おまじない”を書いておくと、コンテンツが画面幅をはみ出していた場合でも横スクロールが発生しないようにできますが、そもそも**なぜhtmlとbodyの両方に"overflow-x: hidden"を設定する必要があるのでしょうか？**

```css:css（例1）
html, body {
  overflow-x: hidden;
}
```

このコードを理解するカギは、CSSの[Overflow Viewport Propagation](https://www.w3.org/TR/css-overflow-3/#overflow-propagation)という仕組みです。詳細は後述するとして、まずは`overflow`プロパティの値を`visible`や`hidden`に変えてhtml要素とbody要素に適用した場合に、境界をはみ出したコンテンツをブラウザーがどのように表示するかを確認してみましょう。

```css:css（例2）
html {
  overflow: visible; /* 初期値 */
}
body {
  overflow: hidden;
}
```

サンプルコードはこちら：
https://codepen.io/kaz_hashimoto/pen/jOKQQQx

`visible`と`hidden`の組み合わせ４通りについてデスクトップ版Chromeで表示すると、結果は図1に示した画面A〜Dのようになりました。

**図1**
![fig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/06a61d23-8fc2-921f-1d6d-dcac12ee82ad.png)
赤い境界線のボックスは&lt;html>要素、青い境界線は&lt;body>要素です。ビューポートとの境界を見分けられるように、&lt;html>と&lt;body>の寸法はウインドウよりも小さくしてあります。

図1の結果を要約すると

| 画面 | &lt;html> | &lt;body> | scroll | はみ出し | はみ出し方 |
|:-:|:-:|:-:|:-:|:-:|:--|
| A | visible | visible | **Yes** | **Yes** | window &lt; _content_ |
| B | visible | **hidden** | No | **Yes** | html &lt; _content_ &le; window |
| C | **hidden** | visible | No | **Yes** | html &lt; _content_ &le; window |
| D | **hidden** | **hidden** | No | No | _content_ &le; body |

※「はみ出し方」の記号の意味は直感的ではありますが、
&lt;：境界で _content_ がクリップされない、&le;：クリップされる。


画面B, Cでは`overflow: hidden`が効いてないように見えますね。CSSに指定したはずの`hidden`の値はどこに行ってしまったのでしょうか？

ここにOverflow Viewport Propagationの仕組みが働いているのです（図2）。

**図2**
![fig2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/b8e7841b-5784-77b9-0b45-13cc25c06f9d.png)

## Overflow Viewport Propagationとは
[CSS Overflow Module Level 3](https://www.w3.org/TR/css-overflow-3/)の[3.5. Overflow Viewport Propagation](https://www.w3.org/TR/css-overflow-3/#overflow-propagation)には次のように書かれています。わかりやすくするため、１つのパラグラフを３つの文に分けて説明します。

**仕様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の場合、&lt;html>に設定した`hidden`値がビューポートに適用されます（図3a）。

**図3a**
![fig3a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/64f49816-9ddf-b023-09b6-d134203460e6.png)

**仕様2**
> However, when the root element is an [HTML] &lt;html> element (including XML syntax for HTML) whose 'overflow' value is 'visible' (in both axes), and that element has as a child a &lt;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]の&lt;html>要素（HTMLのXML構文を含む）であり、その'overflow'の値が（両軸とも）'visible'で、その要素が子として&lt;body>要素を持っている場合、ユーザーエージェントは代わりにその最初の子要素の 'overflow-*' 値をビューポートに適用しなければならない。

たとえば画面Bの場合、&lt;html>に設定した`overflow`の値が`visible`なので、&lt;body>に設定した`hidden`値がビューポートに適用されます（図3bの左）。

**図3b**
![fig3b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/f9682f4c-6bed-2b5c-e13d-3ba010dbfc0d.png)


**仕様3**
> The element from which the value is propagated must then have a used 'overflow' value of 'visible'.

（意訳）
> 値の伝達元の要素はその後、'overflow'の使用値が'visible'でなければならない。

たとえば画面Bの場合、&lt;body>に設定した`hidden`値がビューポートに伝達されたので、伝達元の&lt;body>側が使用値として`visible`になります[^1]。
[^1]:`overflow`プロパティに関して、Window.getComputedStyle()が返す値やChrome DevToolsのComputedタブには計算値が表示されるため、これらの方法では使用値の変化を確認できません。

**図3c**
![fig3c.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/c602ae02-f780-8337-4230-a7a7154a2584.png)

画面B, Cで`overflow: hidden`が効いてないように見える理由もこれでわかりました。つまり、
- &lt;html>と&lt;body>に設定していた`overflow`の値が両方とも使用値として`visible`に変わるため（図3c）、コンテントのテキストが境界からはみ出してしまう。
- ビューポート側には`overflow`の値に`hidden`が適用されるため、&lt;html>の境界からもはみ出したテキストは画面の端でクリッピングされて、画面はスクロールしない。

参考）仕様の解釈はこちらの回答も参考にしました。
stackoverflow#14718319: [Why does overflow-x: hidden make my absolutely positioned element become fixed?](https://stackoverflow.com/questions/14718319/why-does-overflow-x-hidden-make-my-absolutely-positioned-element-become-fixed/14740580#14740580)


## スマホでは状況が違う
画面B, Cでスクロールしないのなら、冒頭の問いに戻って、

横スクロールを防止するために
**なぜhtmlとbodyの両方に"overflow-x: hidden"を設定する必要があるのでしょうか？**

そこで、画面Bと同じページをスマホで表示してみましょう。iOSとAndroidのいくつかのバージョンで試した結果は図4のとおり。画面がスクロールしてしまうものがあります。

**図4**
![visible-hidden_1080p.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/c2a3b269-4d4f-9214-8949-559407b9aa22.png)
※使用したブラウザは、Safari (iOS), Chrome (Android)です。

iOSの場合、15.2はスクロールが発生しますが、同じv.15系でも15.5はスクロールが発生しません。一方、Androidはv10, 11, 12すべてスクロールが発生しました[^2]。
[^2]: さらにiOSの場合と異なり、ページ上部に`position: fixed`で配置してある黒いボックスも一緒に横へスクロールしてしまいます。

これらの事実を踏まえると、デスクトップでもスマホでも確実に横スクロールを防止できるパターンは画面Dしか残されていません。すなわち、**htmlとbodyの両方に"overflow-x: hidden"を設定する必要がある**わけですね。やっと理解できました。

### 補足
ちなみに、CSS仕様書の[3.5. Overflow Viewport Propagation](https://www.w3.org/TR/css-overflow-3/#overflow-propagation)に、スマホでの表示に関する注記があります。しかし、この注記が導入[^3]されるきっかけとなったissue[^4]は図4に示した現象とは異なるため、この現象との関連性はわかりませんでした。
[^3]: Working Draft, 2 December 2021
[^4]: [[css-overflow] Clarify what rect clips the root element with overflow:hidden on mobile environments with dynamic toolbar #5646](https://github.com/w3c/csswg-drafts/issues/5646)

> 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の外側のすべてをクリップしないことがあります（モバイルで発生することがあります）。

[^5]:[定義](https://www.w3.org/TR/css-display-3/#initial-containing-block)によると、初期包含ブロックとはルート要素(html)の包含ブロックだから、[CSS2.1§10.1](https://www.w3.org/TR/CSS2/visudet.html#containing-block-details)の定義によると"it has the dimensions of the viewport"とあるので、「ICBがビューポートより小さい場合」の意味がよくわかりませんでした。