症状:スクロールバーの位置がおかしい・ページが横に少し動く
スマホでもPCでも、ページ全体が横に少しだけ動いてしまう、あるいはページ下部に見慣れない横スクロールバーが出てしまう。原因のCSSを書いた覚えがないのに発生するので、どこを直せばいいのか分からず力業で overflow-x: hidden を body に貼って様子を見る、という対処をしがちな症状です。
この記事は「直し方」よりも「犯人の特定の仕方」に重点を置いています。横スクロールの原因は1つのプロパティに決まっているわけではなく、案件ごとに犯人となる要素が違うため、まずどの要素がはみ出しているかを見つける手順が直し方そのものより重要です。
犯人特定の基本:scrollWidthとclientWidthの差を見る
横スクロールは、必ずどこかの要素の scrollWidth(中身を含む実際の幅)が clientWidth(見た目の表示幅)より大きくなっています。DevToolsで1要素ずつ調べるのは大変なので、ページ内の全要素を一括でチェックして差分がある要素だけを洗い出すのが早道です。
次のスニペットをDevToolsのConsoleに貼り付けると、はみ出している要素を幅の大きい順にリストアップします。
// ページ内の全要素から、scrollWidth > clientWidth の要素を洗い出す
(function () {
const doc = document.documentElement;
console.log('ページ全体のはみ出し量:', doc.scrollWidth - doc.clientWidth, 'px');
const all = document.querySelectorAll('*');
const culprits = [];
all.forEach((el) => {
const diff = el.scrollWidth - el.clientWidth;
if (diff > 0) {
culprits.push({ el, diff });
}
});
// はみ出し量の大きい順に並べる
culprits.sort((a, b) => b.diff - a.diff);
culprits.slice(0, 10).forEach(({ el, diff }) => {
console.log(`+${diff}px`, el);
el.style.outline = '2px solid red'; // 犯人候補を赤枠でハイライト
});
console.log(`はみ出し候補: ${culprits.length}件(上位10件をハイライトしました)`);
})();
コンソールに出力される要素をクリックするとDevToolsのElementsパネルにジャンプします。赤枠が付いた要素のうち、一番外側にあるもの(親要素)が根本原因であることが多いです。子要素にも赤枠が付くのは、親がはみ出した分をそのまま引き継いでいるだけのケースが多いので、リストの上から順に「一番浅い階層の要素」を優先して疑ってください。
下のデモは実際にこのスニペットに近い処理を動かしたものです。幅340pxの要素が枠(点線)からはみ出しており、ボタンを押すと犯人を検出してハイライトします。
「犯人を検出」を押すと、はみ出している要素の scrollWidth と clientWidth の差を計算し、枠を赤くハイライトします。
実務でよくある原因は次の3つ
特定作業で見つかる犯人は、実務ではだいたい次の3パターンに集約されます。以下、それぞれを失敗デモと修正デモで確認します。
原因1:width: 100vw の要素
スクロールバー自体の幅を 100vw が含んでしまい、スクロールバーの分だけ画面幅をはみ出す。
原因2:ネガティブマージンで親の外に飛び出す
margin-left: -40px のような手法で装飾を画面端まで広げようとして、そのまま親要素の外側にはみ出す。
原因3:折り返さない長いURLや文字列
white-space: nowrap の指定や、スペースを含まない長いURL文字列がそのまま1行で表示され、親の幅を押し広げる。
原因1:width: 100vw の要素
100vw は「ビューポートの幅」を意味しますが、この幅には縦スクロールバーの幅が含まれません。つまりスクロールバーが表示されている環境では、100vw は実際の表示領域より数px〜20px程度広くなり、その差分がそのまま横スクロールとして表れます。width: 100% は親要素を基準にするためこの問題が起きませんが、100vw はビューポート自体を基準にするため、スクロールバーの有無を考慮しません。
帯の右端が枠(点線)を超えてはみ出しています。実際のページではスクロールバー幅の分だけ横スクロールが発生します。
親要素の幅を基準にするため、スクロールバーの有無に関係なく枠内に収まります。
/* はみ出す:100vw はスクロールバーの幅を含まない
→ スクロールバーがある環境では画面より広くなる */
.banner {
width: 100vw;
}
/* 直す1:親の幅を基準にする */
.banner {
width: 100%;
}
/* 直す2:どうしても画面幅いっぱいにしたい場合は
スクロールバー幅を差し引く(Chrome, Firefox, Safari 最新版で対応) */
.banner {
width: 100vw;
width: 100dvw; /* モバイルではこちらが実効幅に近い */
}
自分のケースか確認する方法:先ほどの検出スニペットで洗い出した要素のCSSに 100vw(または calc(100vw - ...))が含まれていないか探します。margin-left: calc(50% - 50vw) のような画面幅いっぱいにするテクニックも同じ理由ではみ出すことがあるので、あわせて確認してください。
原因2:ネガティブマージンで親の外に飛び出す
カードやセクションを画面端まで広げる目的で margin-left: -40px のような負のマージンを使うことがありますが、この値は親要素の内側の余白を打ち消すためのものです。親要素自体に横方向の余白(padding)がない状態で使うと、負のマージン分がそのまま親の外側にはみ出し、横スクロールの原因になります。
カードの列が左右の枠を超えてはみ出しています。
親側にpaddingを持たせてから打ち消すことで、枠内に収まります。
/* はみ出す:親に打ち消す余白がないまま負のマージンを付ける */
.card-row {
margin-left: -40px;
margin-right: -40px;
}
/* 直す:親側に同じ幅のpaddingを持たせて打ち消す
(負のマージンは「親のpaddingを打ち消す」用途に限定する) */
.card-row-wrap {
padding-left: 40px;
padding-right: 40px;
}
.card-row {
margin-left: -40px;
margin-right: -40px;
/* 打ち消した結果、実質はみ出さない */
}
自分のケースか確認する方法:検出スニペットの結果で見つかった要素に margin-left や margin-right の負の値が付いていないか確認します。Computedパネルで負のマージンを一時的に0に戻してみて、はみ出しが消えれば犯人確定です。
原因3:折り返さない長いURLや文字列
スペースを含まない長い文字列(URL、トークン、連番のIDなど)は、通常のテキストの折り返しルールでは改行されません。テーブルのセルやカードの中にこうした文字列が入ると、要素がその文字列の幅まで広がり、親要素や画面全体をはみ出させます。white-space: nowrap を明示していなくても、長い英数字の連続は自動では折り返されない点に注意してください。
長いURLが1行のまま伸び、枠をはみ出します。
長い文字列が枠の幅で折り返されます。
/* はみ出す:スペースのない長い文字列は自動では折り返されない */
.long-text {
/* 何も指定しない、または white-space: nowrap */
}
/* 直す:単語の途中でも折り返しを許可する */
.long-text {
overflow-wrap: break-word; /* または word-break: break-all; */
}
/* テーブルのセルなど、幅を固定したうえで折り返したい場合 */
.long-text-cell {
max-width: 240px;
overflow-wrap: break-word;
}
overflow-wrap: break-word は単語の区切りを優先しつつ、はみ出す場合のみ単語の途中で改行します。word-break: break-all はより強制的にどこでも改行するため、日本語混じりの文章では不自然な位置で切れることがあります。URLやIDなど英数字だけの文字列には break-all、日本語を含む文章には overflow-wrap: break-word が向いています。
自分のケースか確認する方法:検出スニペットで見つかった要素の中身が、スペースのない長い文字列(URL・ハッシュ値・連番ID)でないか確認します。テーブルのセルで起きている場合は、該当の td に max-width と overflow-wrap: break-word を足すのが定番の対策です。
それでも見つからないときの追加チェック
検出スニペットで犯人が見つからない、または複数の要素が絡み合って特定しづらい場合は、次の手順を試してください。
手順1:body配下の要素を1つずつ非表示にして絞り込む
DevToolsで body の直下の要素を上から順に非表示(display: none)にし、横スクロールが消えるタイミングを見る。消えた時点の要素の中に犯人がいる。
手順2:擬似要素(::before / ::after)も疑う
検出スニペットの document.querySelectorAll('*') は擬似要素を拾えません。装飾用の ::before / ::after に大きな width や負の left を指定していないか、Elementsパネルで個別に確認する。
手順3:絶対配置(position: absolute)の子要素を疑う
絶対配置された要素は通常のレイアウト計算から外れるため、検出スニペットのハイライトだけでは気づきにくい。left や right にマイナス値や大きな値が入っていないか確認する。
overflow-x: hidden で隠すのは最終手段
body や特定のラッパーに overflow-x: hidden を貼ると横スクロールバー自体は消えますが、はみ出した要素そのものは直っていません。position: sticky が効かなくなる、印刷時にコンテンツが切れる、将来別の要素が同じ原因でまたはみ出すといった副作用が起きやすく、根本原因を残したまま症状だけ隠す対処です。
overflow-x: hiddenは症状を隠す薬であって、原因を治す薬ではない。検出スニペットで犯人を見つけて直すほうが、後から効いてくる。
納期の都合でどうしても一時対処が必要な場合は、overflow-x: hidden を貼った箇所と、後で直すべき犯人要素をコメントで残しておくと、後日の修正が楽になります。
まとめ
横スクロールのトラブルは、原因のCSSプロパティを覚えるより先に犯人の要素を特定する手順を持っておくほうが早く解決します。DevToolsのConsoleで scrollWidth と clientWidth の差を一括チェックし、見つかった要素の親をたどるのが基本の流れです。
検出スニペットで犯人候補を洗い出す → 一番浅い階層の要素を疑う → 100vw・負のマージン・折り返さない文字列の3パターンに当てはまらないか確認する。
ここで扱った3パターン以外にも、flexboxのmin-width:autoが原因で横スクロールになるケースもあります。テーブルを含むレイアウトで横スクロールが出る場合は、flexboxではみ出す・縮まない原因と直し方もあわせて確認してください。
同じシリーズの関連記事
「効かない・動かない」トラブル解決シリーズの記事です。症状から原因を切り分けたいときにあわせてどうぞ。
関連するUI事例
横幅の制御やテーブルのレイアウトを、実際に動くコードで確認できる事例です。