20
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

デザインの実装は正しいのに実機で崩れる ── モバイルキーボードとビューポートの落とし穴

20
Last updated at Posted at 2026-07-06

はじめに

フロントエンドのバグには、ときとして「コードでは直せない、ブラウザ構造上の限界」に行き着くものがあります。そういうとき本当に必要なのは、単にHTML/CSS/JSをこね続けることではなく、「どこまでがWebで直せる範囲か」を適切に、素早く見極めて、限界の先はUXやプロダクトの判断に委ねることだと思っています。

この記事は、その一例です。モバイルのソフトウェアキーボードで起きたスクロール不具合を、原因まで実測で特定したうえで、最終的に「レイアウトでは直せない」と判断するまでの記事になります。きれいな解決策の紹介ではありません。

(ちなみに筆者はふだん、UIデザインとフロントエンド実装の両方に関わっていますが、こうした「デザイン上は一見うまくできているのに、実機では崩れる」ような問題は、その境目で起きるため調査を担当することが多く、今回もその一件でした。)

なぜこの手のバグは厄介なのか

何が難しいかというと、

  • PCのブラウザでは再現しない(ソフトウェアキーボードが無いので当然)
  • Chromeのデバイスエミュレーションでも再現しない(ソフトウェアキーボードを「出した状態」を再現できない)
  • iOS Safari と Android、さらにアプリ内WebViewでそれぞれ挙動が違う場合がある

つまり「手元で直せない」「直したつもりでも実機で確認するまで分からない」ため、修正と確認の往復に時間がかかり、なかなか前に進まない領域です。

同じ症状に直面した人が、原因の切り分けと「どこで諦めるか」の判断を早められればと思って書いています。

何が起きたか ── チャットの会話が伸びると、先頭に戻れなくなる

問題が起きたのは、画面全体を覆うチャット型のドロワーでした。構造はチャットUIの定石そのものです。

fig1-fixed.png

CSSでは、ドロワー本体を position: fixed; height: 100% で全画面に固定し、中身を flex-direction: column で「固定ヘッダー / 伸びるスクロール領域 / 固定入力欄」に分ける、ごく普通の作りです。ソフトウェアキーボードのないPCでは問題なく動いていましたが、iOS・Androidなどのモバイルでは特定の条件下で良くない挙動をします。

症状は、iOS・Android実機で入力欄をタップしてキーボードを出した状態で、次の条件のときだけ現れます。

  • 会話が短い(チャット領域があふれていない) … 問題なし。上にスクロールすれば会話の先頭まで見える
  • 会話が長い(チャット領域があふれている)上にスクロールしても、会話の先頭まで届かない。数百px手前で頭打ちになる

キーボードを出していないとき(全画面)は、どれだけ会話が長くても普通にスクロールできます。「キーボード表示中」かつ「会話が長い」ときだけ、先頭が見られなくなる。この条件の噛み合わせが、原因の切り分けを難しくしていました。

なぜ起きるのか ── 2つのビューポートと、入れ子スクロールの横取り

原因は2つの要素の合わせ技でした。順に説明します。

前提:レイアウトビューポート と ビジュアルビューポート

まず、ブラウザにはサイズの異なる2種類の「ビューポート(表示領域)」があります。

何を指すか キーボードを出すと
レイアウトビューポート(layout viewport) CSSが「画面の大きさ」として基準にする領域。position: fixedheight: 100% はこれを基準にする 変わらない(iOSの場合)
ビジュアルビューポート(visual viewport) ユーザーが実際に「見えている」領域。キーボードで隠れた分だけ小さくなる 縮む/上にずれる

fig2-fixed.png

iOSは、キーボードが出てもレイアウトビューポートを縮めません。縮むのはビジュアルビューポート(見えている領域)だけ。そしてブラウザは、入力エリアが下部にあると、フォーカスした入力エリアを中央に寄せようと、見えている領域を上へパン(スクロール)させます。

fig3-fixed.png

このとき position: fixed; height: 100% のドロワーはレイアウトビューポート基準なので、キーボードが出たことを知らず、画面全体(キーボードの裏まで)を占め続けます。結果として、ドロワーの上端はキーボードのパンによって「見えている領域」より上へ押し出されます。これが崩れの起点であり、事象を分かりにくくする原因です。

fig4-fixed 1.png

本題:入れ子スクロールが上スワイプを「横取り」する

チャット領域は、それ自体が overflow-y: auto を持つ入れ子の(チャットエリア内の)スクロール領域です。加えて、背面への影響を防ぐために overscroll-behavior: contain(背面へのスクロールの連鎖を止める)も付けていました。

ところが、この構成が会話量によって裏目に出ます。

  • 会話が短い(あふれていない):チャット領域は自分ではスクロールできない(チャット領域にスクロールバーが出ていない状態)。だから上スワイプは外側に渡り、ドロワー全体がパンして動く。結果、先頭まで見える
  • 会話が長い(あふれている):チャット領域が自分でスクロール可能になる(チャット領域にスクロールバーが出ている状態)。だから上スワイプは内部スクロールに横取りされ、overscroll-behavior: contain によって外側への伝播も止まる。外側(ドロワー全体)は動かない。

fig5-fixed 1.png

そして肝心なのは、内部スクロールをいっぱいまで動かしても、チャットエリア内がスクロールされるだけで、会話の先頭は依然として画面の外に残ってしまうという点です。

あふれてスクロール可能になったチャット領域が上スワイプを奪ってしまうため、動かせるのはチャット領域の内部スクロールだけになります。ドロワー全体を下げて先頭を画面内へ引き戻す動き(外側パン)はもう起きません。そして内部スクロールでは、すでに画面の外へ出てしまった先頭までは戻せない——これが「先頭に戻れない」の正体でした。

一言でいうと:「あふれた入れ子スクロールが上スワイプを横取りするので、画面の外にある先頭に届かない」 です。

どう直そうとしたか

原因が分かってからは、いくつかの手を試しましたが、結論から言うと、JSやCSSでレイアウトをいくら修正しても、納得のいく実装にはたどり着けませんでした。却下した案とその理由を残します。

却下案1:チャット領域を非スクロール化する(height: auto / overflow: visible

入れ子スクロールが横取りするなら、チャット領域の内部スクロールをやめて、ドロワー全体を動かすスクロールだけに一本化すればいい、という発想です。しかし position: fixed の中身は、ページのスクロールでは送れません。チャット領域を伸ばすと、下端にある入力欄が画面外へ出て、入力欄へ到達不能になりました。✗

却下案2:overscroll-behavior を解除して連鎖を許可する

内部スクロールが上端で止まったあと、その先をドロワー全体のスクロールへ連鎖させれば先頭を出せるはず、という案です。先頭は出せるようになりましたが、さらに上へスクロールするとドロワー全体がテキスト入力エリアごと下がり、今度は入力欄がキーボードの下へ隠れてしまう。加えてスクロールが二段階に感じられ(実際スクロールバーが2つ出ます)、操作感がかなり不自然になりました。✗

唯一の道は「コンテナを可視領域に合わせ続ける」。

キーボードを出したまま成立させる方法は、原理的に1つしかありません。チャットのコンテナ(ドロワー)を、JSで「見えている領域」の大きさ・位置に合わせ続けることです。「見えている領域」は window.visualViewport で取得できるので、その値をドロワーの高さ・位置に当て込みます。こうすればチャット領域の上端が可視領域の中に収まり、先頭にも到達できるようになります。

// ドロワーを、見えている領域(visualViewport)の大きさ・位置に合わせる
const vv = window.visualViewport;
drawer.style.height = vv.height + 'px';   // 高さを可視領域に合わせる
drawer.style.top = vv.offsetTop + 'px';   // 位置を可視領域の先頭に合わせる

理屈は正しいはずでした。ところが実機では安定しませんでした。

  • キーボードの種別変更(日本語↔英語)や予測変換バーの増減で高さが変わるたびにリサイズが走り、その度に崩れる。
  • キーボードと入力欄の間に出るOS標準のバー(「キーボードを閉じる」ボタンやパスワード一覧ボタンが並ぶ、いわゆる アクセサリバー(input accessory view))の付近がスクロールできてしまい、そこを動かすとレイアウトビューポートが一気に崩れる(このスクロールもWeb側からは制御できない)

さらに iOS に限り、テキストエリアの下にスペースが空いてしまう事象も発覚しました。これは WebKit自身の既知バグでした。

WebKit Bug 292603 ── キーボード絡みで画面下部に不要な「スクロール可能な空白領域」ができる。ステータスは OPEN(未解決)、iOS 15〜26で再現。overflow / overscroll-behavior / viewport単位のhackでは抑止できないと報告されている。

つまり、コンテナを可視領域に合わせても、Web側からは原理的に安定させられない。実装が不十分だから直らないのではなく、土台のブラウザが完全に対応しきれていない。ここで「レイアウトで完璧に成立させる」ことは断念しました。

根本的な問題

キーボードを出したまま、「ヘッダー(上端)」と「入力欄(下端)」を同時に可視領域へ収めることは、コンテンツが可視領域より高い限り、レイアウトだけでは実現できない。

外側パンは全体を一緒に動かすので、ヘッダーを見せれば入力欄が沈み、入力欄を見せればヘッダーが隠れる。両立させる唯一の道が「コンテナを可視領域(visualViewport)に合わせる」ことで、それがブラウザの都合で信頼できない以上、要件を満たした実装にたどり着くことはできない、というのが結論でした。

チェックポイント・学び

今回の対応から得たポイントをまとめます。

1. 「キーボード表示中だけ」「特定条件だけ」出る崩れは、ビューポートのズレを疑う

position: fixed / height: 100% はレイアウトビューポート基準で、iOSではキーボードで縮みません。「PCでは出ないのにモバイル(iOS/Android)だけ」「短いと出ないのに長いと出る」といった条件付きの崩れは、まずレイアウトビューポートとビジュアルビューポートのズレを疑うのが近道です。

2. 入れ子スクロール × overscroll-behavior: contain は、条件で挙動が裏目に出る

あふれていないときは外側にスクロールが渡り、あふれるとその場で横取りする。良かれと思って足した overscroll-behavior: contain が、キーボード表示時には「先頭に戻れない」の一因になっていました。入れ子スクロールを使うUIでは、オーバーフローの有無で挙動が変わる前提で設計する必要があります。

3. 実機依存の視覚バグは「推測」ではなく「実測値」で詰める

scrollHeight / clientHeight / visualViewport.height / offsetTop の数値を実機で取得して、「内部スクロール量がxxxpxしかない」「チャット領域の高さが可視領域を上回っている」と分かり、原因が確定しました。推測でCSS/JSをいじって実機確認を往復するより、各数値を採るほうが圧倒的に速い。

4. WebKitのバグトラッカーを確認する

「自分のコードが悪い」と決めつけて直し続ける前に、ブラウザ側の既知バグでないかを確認しましょう。Web側で原理的に直せない問題に時間をかけ続けないために。今回は WebKit #292603 がOPENのままだと知って、解決を諦める判断ができました。

5. 「レイアウトで直す」に固執せず、UXで要件を外す選択を持つ

「キーボードを出したまま全部見せる」という前提を守ろうとする限り、この問題は解けません。前提のほうを外す(チャットを読むときはキーボードを閉じるとする)と、途端に全ブラウザで確実に動く解になります。UIの制約は、実装で殴るより設計で回避するほうが堅牢なことがあります。

おわりに

今回は、原因は実測で特定できたものの、最終的にCSS/JSのレイアウト修正の範囲では直せないと結論づけ、UX側で対応する方針とし、現状は保留にしました。

ただ振り返ると、これは設計判断の誤りというより 「モバイルのキーボード × Webのビューポート機構」という領域そのものが、CSS/JSだけで直そうとするには原理的に難しい のだと思います。WebKit自身がバグを直しきれていないのが、その証拠とも言えます。position: fixed の全画面ドロワー+入れ子スクロールという構造は、チャットUIの定石であって間違いではありません。それでもiOS/Androidのビューポート挙動が信頼できない以上、キーボードを出したまま「先頭もチャット欄も入力欄も全部見せる」レイアウトを安定させることはできませんでした。

UIの「見た目・体験の崩れ」を、ブラウザの実装レベルで原因を切り分け、最後はUX設計で落とす——こういうフローは、Webアプリ開発ではよく直面する課題で、デザインとエンジニアリングのちょうど境界に立っていると、常々頭を悩ます類のものだと感じています。

実機でしか出ないバグは、修正と確認の往復に時間がかかり、対応のコストが見えにくいものです。しかし「これはブラウザ側の制約で、Webからは直せない」と切り分けて説明できれば、レイアウトでの解決を諦めてUXへ倒すことで、無駄な時間を費やさずに済みます。もし同じような問題に直面したときは(あるいはそういう方がいたら)、原因の切り分けと諦めどころの判断にかける時間を、少しでも減らせたら幸いです。


関連バグ:WebKit Bug 292603(キーボード表示時の下部空白領域・OPEN)/ 関連 #191204(キーボード表示時のビューポート機構の破綻)


KIYOラーニング株式会社について

当社のビジョンは『世界一「学びやすく、分かりやすく、続けやすい」学習手段を提供する』ことです。革新的な教育サービスを作り成長させていく事で、オンライン教育分野でナンバーワンの存在となり、世界に展開していくことを目指しています。

プロダクト

  • スタディング:「学びやすく・わかりやすく・続けやすい」オンライン資格対策講座
  • スタディングキャリア:資格取得者の仕事探しやキャリア形成を支援する転職サービス
  • AirCourse:受け放題の動画研修がついたeラーニングシステム(LMS)

KIYOラーニング株式会社では一緒に働く仲間を募集しています

20
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?