スマホだけ横にちょっとスクロールできる現象を直す方法(CSS overflow-x clip)

概要

今回はサイトを作っているときにスマホでだけ左右にほんの少しスクロールできてしまう現象(iOS Safariのelastic bounce)の原因の調べ方と直し方について紹介していきます。

サイト開発をしていると、PCではちゃんと動作しているのに、スマホで触ったときだけ「左右にちょっとだけスクロールできて、指を離すとスーッと中央に戻る」という謎な挙動に出くわすことってありますよね(- -;

壊れているわけではないけれど、触ったときの違和感が大きい。

レイアウトが崩れているわけではないので、ぱっと見では原因が分からず、説明も難しい、、、
これは結構ハマるやつです。
自分はハマりました。

検索しても「横スクロールを消す」系の記事は出てくるものの、「なぜ両方向にバウンスするのか」「html,bodyにoverflow:hiddenを当ててはいけないのはなぜか」まで踏み込んだ説明が意外と少ないんですよね。

そこでこの記事では、症状の整理から原因の特定方法、overflow-x: clip での最小修正、hidden との違いと副作用までを順番にまとめておきます。
同じ症状で困っている方の参考になればと思います(^^b

それではやっていきましょう(^^!

この記事はCSSの実装をしている方向けの記事となります。
具体的なソースを一応記載しているので、是非試してみてください!

目次

何が起きていたか(スマホで横スクロールできる症状)

まず症状から整理します。

  • スマホでサイトを表示すると、左右にほんの数pxだけスクロールできてしまう
  • 指を離すとスーッと中央に戻る(=本来は固定で居てほしい状態)
  • PCで見たときは何も起きない(横スクロールなんてできない)
  • DevToolsのスマホエミュレートではこんな操作しないので、気づきにくい

ぱっと見「右側がはみ出してる?」と思うのですが、実際にスクロールしてみると左にも右にもバウンスするので「両方スクロールできるってどういうこと?」となります。

ここがちょっと厄介で、原因の見立てを誤るとかなり時間を溶かします(- -;

実際これってどう調べたらいいのかわからないので、本当に沼るんですよね…

スクロールバーが出ないスマホだと、はみ出しているのか、それともブラウザの仕様で揺れているだけなのかの区別がつかない、というのが沼の入り口だと思います。

なぜ起こるのか — iOS Safariのelastic bounce

結論から言うと、iOS Safariはページの中身がviewportより少しでも広いと、両方向に弾むスクロール(elastic bounce)を許可します。

たとえ右に2pxはみ出しているだけでも、左にも右にもバウンスできてしまうのです。

つまり「両方スクロールできるように見える」状態は、実際には片側だけが数pxはみ出している、というケースがほとんどです。

ここを誤解して「左右両方に余白が漏れているのでは?」と探し始めると、存在しない左側のはみ出しを延々と探すことになります。
「バウンスは両方向、はみ出しは片方向」とだけ覚えておくと、調査の方向がブレません。

はみ出しの原因になりやすいパターン

経験上、犯人になりやすいのは以下です。

  • 装飾要素を right: -20px のように負の値で配置している(吹き出し、リボン、影など)
  • position: absolute の浮遊アイコンを left: 0 などコンテナの端ピッタリに置いている
  • drop-shadowbox-shadow がコンテナの外にはみ出している
  • 画像やSVGが想定より大きく描画されている
  • width: 100vw を使っていて、スクロールバー幅のぶんだけ横に溢れている

PCではグリッドの隙間や余白に吸収されて気づきません。

スマホで1カラムになった瞬間、装飾がviewport端を数pxだけ突き抜けて発覚する、というのが定番パターンですね。

ちなみに装飾を後から足したタイミングで再発しがちなので、「以前は出てなかったのにいつの間にか出るようになった」というケースも多いです。

特に最後の 100vw は見落としやすくて、vw はスクロールバーを含む幅で計算されるため、デスクトップ表示で常時スクロールバーが出る環境だとその幅ぶんだけ横に溢れます。
横幅いっぱいのフルブリードな帯を作りたいときに踏みがちな罠ですね。

横スクロールの原因要素を特定する方法

直す前に、どの要素がはみ出しているのかを特定できると話が早いです。
ここをスキップして当てずっぽうで overflow-x を盛ると、別の場所で副作用が出たりします。

DevToolsのコンソールで一発で洗い出す

手軽なのは、ブラウザのコンソールに次のスニペットを貼って、viewportより広い要素を列挙する方法です。

console

1
2
3
4
5
6
7
const docW = document.documentElement.clientWidth;
document.querySelectorAll('*').forEach((el) => {
const r = el.getBoundingClientRect();
if (r.right > docW + 1 || r.left < -1) {
console.log(Math.round(r.left), Math.round(r.right), el);
}
});

right がviewport幅を超えている要素、あるいは left がマイナスになっている要素が犯人候補として表示されます。

ログに出た要素をホバーすれば、DevTools上でその要素がハイライトされるので、どの装飾が突き抜けているのか一目で分かります。

アウトラインで全要素の境界を可視化する

もう一つ、視覚的に当たりをつけたいときは全要素に枠線を引いてしまうのが手軽です。

debug.css

1
2
3
* {
outline: 1px solid red;
}

これだけでどのボックスがコンテナからはみ出しているかが赤枠で浮かび上がります。

実機のSafariで確認したいときは、MacのSafariとiPhoneをケーブルでつなぎ、「開発」メニューからリモートインスペクタを開くと、実機の描画をそのままDevToolsで触れます。
エミュレータだとelastic bounceは再現しないので、最終確認は実機 + リモートインスペクタが確実です。

直し方:親セクションに overflow-x: clip; を1行

原因が掴めたら、直すのは簡単です。
はみ出しを許容している親セクションに overflow-x: clip; を1行足すことです。

style.css

1
2
3
4
.hero {
/* 既存のスタイル */
overflow-x: clip;
}

これだけです。

ポイントはhidden ではなく clip を使うこと。

ここを hidden にしてしまうと、別の副作用(後述)を引き起こす可能性があるので注意してください。

overflow-x: hidden と clip の違い

プロパティ スクロールコンテナを作る? sticky への影響
overflow-x: hidden 作る position: sticky が効かなくなることがある
overflow-x: clip 作らない sticky もそのまま動く

ここが大きな違いです。

overflow-x: hidden はその要素を「スクロールコンテナ」として扱ってしまうため、子孫要素の position: sticky が想定通り動かなくなることがあります。

一方で clip単純に「はみ出した分を視覚的に切る」だけで、内部のレイアウトには手を加えません。

つまりstickyヘッダーやサイドバーなどがあるサイトでは、hidden ではなく clip を選ぶのが安全です。

ちなみにoverflow-x: clip はSafari 16以降を含むモダンブラウザはすべて対応しているので、今となってはほぼ気にせず使えます(^^b

clip のはみ出す向きを調整したいときは overflow-clip-margin

clip で切る位置を少しだけ緩めたいときは、overflow-clip-margin で「どこまでなら表示を許すか」を指定できます。

style.css

1
2
3
4
.hero {
overflow-x: clip;
overflow-clip-margin: 8px;
}

たとえばうっすら外に出る box-shadow だけは残したいけれど、横スクロールは消したい、というときに効きます。

とはいえ多くのケースでは指定なし(=切り口ピッタリで切る)で十分なので、まずは1行の overflow-x: clip; だけで様子を見るのがおすすめです。

どこに当てるか — html, body への適用は避けるべき?

便利な overflow-x: clip ですが、html, body に当てるのは横着なのでやめましょう。

理由は単純で、サイト全体に影響するため、思いもよらない動作を引き起こす可能性があるからです。

たとえば後から「画面端からふわっと出てくるアニメーション」みたいなものを実装したくなったとき、大元で overflow-x: clip がかかっていると意図せず切られてしまう、ということが起きます。

こういったときはなんでだ?とかなり調査に時間がかかることが多いので、脳死でhtmlやbodyに適用するのは危険だと思います。

さらに html 要素にスクロール系のプロパティを当てると、position: sticky のスクロール基準が変わってしまったり、scroll-behavior: smooth のアンカースクロールが噛み合わなくなったりと、思わぬところに波及します。
「全体に効くプロパティほど、後から外しづらい」というのは意識しておきたいところですね。

正解:問題が起きているセクション単位で当てる

問題が起きているセクションだけに overflow-x: clip を当てるのが正解です。

今回のケースでは、ファーストビュー全体を包むセクションに1行足すだけで解決しました。

上でも言ったようにメリットは下記のとおりです。

  • 影響範囲を最小限にできる
  • 他のページや将来の装飾に影響しない
  • 装飾を後から増やしても再発しにくい
  • どこで何を抑え込んでいるかコードを読めば分かる

装飾要素の位置を一つずつ手直しするより、親側で受けるほうがコードもシンプルで、後から装飾を増やしても再発しません。

これが今回一番伝えたいポイントです(^^

再発を防ぐために — viewport設定も合わせて確認

横スクロールの相談で意外と多いのが、viewportmeta タグ設定そのものが原因になっているケースです。

最低限、head に次の指定が入っているかは確認しておきましょう。

head.html

1
<meta name="viewport" content="width=device-width, initial-scale=1">

width=device-width が抜けていると、スマホが勝手にPC幅でレンダリングしてから縮小表示するため、横スクロールやレイアウト崩れの温床になります。

ここが正しく入っている前提で、それでも数pxだけ横に揺れる、というのが今回扱ってきた「装飾のはみ出し」起因の症状ですね。

逆にページ全体がガッツリ横スクロールする場合は、まずこの viewport 設定と、横幅を固定値(px)で組んでいる箇所を疑うと切り分けが早いです。

まとめ

今回のポイントを整理しておきます。

  • 「スマホでだけ左右にちょっとスクロールできる」現象は、たいてい数pxのはみ出しが原因
  • バウンスは両方向に見えても、はみ出しているのは片側だけのことが多い
  • 犯人は装飾要素の負の positionbox-shadow のはみ出し、width: 100vwが多い
  • 原因要素はコンソールスニペットや * { outline } で特定すると早い
  • 直し方は親セクションに overflow-x: clip; を1行足すのが最短で副作用も少ない
  • hidden ではなく clip を選ぶ(sticky を壊さないため)
  • html, body への一括適用ではなく、問題が起きているセクション単位で当てる

特に最後の「セクション単位で当てる」は、後から装飾を足したときの再発防止にもなるので意識しておくと楽です(^^b

CSSは「グローバルに当てる方が手っ取り早く感じる」場面が多いですが、影響範囲を狭く保つほうが結局メンテが楽になる、というのはよくある話ですね。

以上となります。
スマホ実機で確認してみないと気付かないバグは多いので、リリース前に一度触ってみるのをお勧めします!
それではお疲れ様です!