stickyのtop:0が残ったままfixedにすると下部固定バーが全画面化する
※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。
結論
position: stickyのtop: 0を解除せずにposition: fixedへ切り替えると、bottom: 0と両立してtopからbottomまでの全高にストレッチし、意図しない全画面要素になる。
結論
position: sticky用に置いたtop: 0を解除しないまま、メディアクエリ側でposition: fixedに切り替えると、bottom: 0と両立して要素の高さがtopからbottomまでの全高にストレッチする。 実際に踏んだのは、PC用にタブバーをstickyで上部固定していたコンポーネントを、モバイル用のメディアクエリでfixedの下部固定バーに変えたケースだった。モバイル側ではbottom: 0だけを新しく指定したが、ベースルールのtop: 0はメディアクエリで上書きしない限り生き続ける。結果として下部固定のつもりだったタブバーが画面の上端から下端まで伸び切り、他の操作をすべて覆ってしまった。直し方はモバイル用ブロックにtop: autoを1行足すだけだが、CSSの構文としては最初から正しいため、実機かブラウザで見るまで気づけない種類の不具合だった。
症状
このコンポーネントはPCでは画面上部にタブバーを表示し、スクロールしても追従するようposition: stickyで固定している。モバイル(640px以下)では画面下部の固定ナビに切り替える設計で、position: fixedとbottom: 0をメディアクエリ内で指定していた。
2026-06-15に本番で報告されたのは、モバイルで下部固定タブバーが画面全体にストレッチし、他の要素をすべて覆って操作できなくなる事故だった。ページのDOM構造やJavaScript側のロジックには変更が無く、CSSのみが疑わしい状態だった。
原因
修正前のベースルール(PC用)は次の内容だった。
/* apps/web/app/app/editor/[classId]/_components/ClassEditorShell.module.css(修正前・13-25行目) */
.tabBar {
display: flex;
gap: 0.25rem;
padding: 0.25rem;
margin-bottom: 1rem;
background: #f7f8fa;
border-radius: 0.6rem;
/* PC: スクロールしてもタブを上部に固定 */
position: sticky;
top: 0;
z-index: 30;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
そしてモバイル用のメディアクエリ(修正前)が以下だった。
/* apps/web/app/app/editor/[classId]/_components/ClassEditorShell.module.css(修正前・56-68行目) */
@media (max-width: 640px) {
.tabBar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 50;
margin-bottom: 0;
border-radius: 0;
border-top: 1px solid #e5e7eb;
background: #fff;
padding: 0.25rem 0.25rem calc(0.25rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
}
CSSのカスケードでは、メディアクエリが上書きしないプロパティはベースルールの値がそのまま生きる。モバイル用のブロックはposition・left・right・bottomなどは上書きしているが、topには一切触れていない。そのためベースルールのtop: 0がモバイルでも有効なまま残り、.tabBarはposition: fixed; top: 0; bottom: 0;という組み合わせで描画されていた。
position: fixedの要素にtopとbottomを両方指定すると、高さの指定が無い限りその間いっぱいまでストレッチする。つまりこのタブバーは、画面上端(top: 0)から画面下端(bottom: 0)までの全高を占めるボックスになっていた。中身のタブ自体はflexで上詰めに並んでいたため見た目には気づきにくいが、要素そのものは画面全体を覆うクリック領域を持っており、他のUIへのタップやクリックを吸収してしまっていた。
直し方
モバイル用の.tabBarにtop: autoを1行追加した。
/* apps/web/app/app/editor/[classId]/_components/ClassEditorShell.module.css(修正後・56-72行目) */
@media (max-width: 640px) {
.tabBar {
position: fixed;
/* ベースの sticky 用 top:0 を必ず解除する。fixed で top:0 と bottom:0 が両立すると
* 高さが viewport 全高にストレッチし、フッターが全画面を覆って全操作をブロックする
* (2026-06-15 本番事故)。下部固定だけにするため top:auto。 */
top: auto;
left: 0;
right: 0;
bottom: 0;
z-index: 50;
margin-bottom: 0;
border-radius: 0;
border-top: 1px solid #e5e7eb;
background: #fff;
padding: 0.25rem 0.25rem calc(0.25rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
}
top: autoにすると、ブラウザはtopの値を「制約しない」ものとして扱う。bottom: 0だけが有効な制約として残るため、要素の高さは中身(タブのぶん)だけに戻り、画面下端に貼り付いた本来の下部固定バーになる。ベースルールのsticky用top: 0はPC表示にはそのまま必要なので、ベース側は変更せず、モバイル側で明示的に打ち消す形にしている。この修正はCSSのみで、スキーマや型、ロジックの変更は含まれていない。
再発防止
この不具合が実機に出るまで気づけなかったのは、position: fixed; top: 0; bottom: 0;という組み合わせがCSSの構文としては何も間違っていないためだ。TypeScriptの型チェックやビルドはCSS宣言の値の妥当性までは見ておらず、topとbottomが同時に効いた状態でも警告は出ない。ブラウザは指定されたとおりに解釈してストレッチしたボックスを描画するので、レンダリング結果を実際に確認しない限り検出できない種類の不具合だった。
positionを切り替えるメディアクエリを書くときは、切り替え先のpositionが依存するオフセットプロパティ(top・right・bottom・left)のうち、ベースルールで設定済みのものが残っていないかを合わせて確認する必要がある。今回の修正では、値を明示的に打ち消すtop: autoをコメント付きで残すことで、なぜこの1行が必要なのかを次に触る人が再び調べ直さずに済むようにした。
よくある質問
Q1なぜbottom:0を指定しただけで画面全体を覆ったのですか?
positionがfixedの要素は、topとbottomを両方指定するとその間いっぱいまで高さがストレッチします。このコンポーネントはPC用の.tabBarにposition: sticky; top: 0;がベースルールとして設定されており、モバイル用のメディアクエリでposition: fixedとbottom: 0を追加した際にtopの値がリセットされず、ベースのtop: 0がそのまま引き継がれていました。結果としてfixed要素がtop: 0からbottom: 0までの全高、つまりviewport全体を覆うボックスになりました。
Q2なぜビルドやテストで気づけなかったのですか?
この組み合わせはCSSの構文としては正しく、TypeScriptの型チェックやビルドの検査対象にもなりません。ブラウザは指定された通りにtop: 0とbottom: 0を両立させて描画するため、実機かブラウザでレンダリング結果を確認しない限り、静的な検査だけでは全画面化に気づけませんでした。
Q3直し方はどこを変えましたか?
モバイル用のメディアクエリ内の.tabBarにtop: autoを1行追加しました。position: fixedのままbottom: 0だけを効かせ、topの値をブラウザに委ねることで、要素の高さは中身のぶんだけに戻ります。
確認した環境
- Next.js ^16.0.0 / CSS Modules
- 2026-06-15 に本番で発生、同日中に修正
この記事の根拠
- CSSファイル 13〜25行目コミット 3b4ea03
- CSSファイル 56〜68行目コミット 3b4ea03
- CSSファイル 56〜72行目コミット 98d3816
本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。