こんにちは!2ちゃんねる互換掲示板システム「 つゔぁいちゃんねる(2ch.biz) 」の開発録、第39回です。
Webアプリケーションを開発していて、全フロントエンドエンジニアが一度は頭を抱え、枕を濡らす最大の魔境――。
それこそが 「スマートフォンのソフトウェアキーボード出現に伴うビューポート崩壊」 です。
デスクトップのブラウザでは完璧に動いていたP2Pチャット画面を、iPhoneやAndroidの実機で開いた瞬間、私たちは目を疑う光景に直面しました。
「画面を開いた時点で、アドレスバーのせいで最下部のメッセージ入力欄が半分見切れている」
「入力欄をタップした瞬間、キーボードが下から競り上がってきて、 入力欄がキーボードの裏側に完全に隠れてしまう! 」
「画面が勝手にズームして、右側の参加者リストがどこかへ吹き飛んだ!」
本稿では、iOS Safari と Android Chrome という2大巨頭の変態的挙動をねじ伏せ、ネイティブチャットアプリと寸分違わぬ「キーボードに吸い付く入力バー」を実現した死闘の全記録を解説します!
1. モバイルWebを襲う「4大ビューポート地獄」
ネイティブアプリ(SwiftやKotlin)であれば、OSがキーボードの開閉に合わせてレイアウトを自動調整してくれます。しかし、ブラウザ上のWebアプリでは、以下の4つの罠が同時に襲いかかってきます。
特に最悪だったのが 「② 入力欄がキーボードの裏に隠れる現象」 です。自分が今何をタイピングしているのかが一切見えず、変換候補すら確認できない「盲目タイピング」を強いられていたのです。
2. 解決へのアプローチ:最新Web標準とAPIの総動員
この複雑怪奇な問題を解決するため、私たちは単一のハックに頼るのではなく、4つの最新技術を組み合わせて防壁を築きました。
3. 実装の全貌
(1) Viewportメタタグの進化:interactive-widget の指定
モダンブラウザ(Chrome 108+ 等)でサポートされた最新の属性 interactive-widget=resizes-content を Viewport メタタグに追加しました。
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content, viewport-fit=cover">
-
interactive-widget=resizes-content: キーボードが出現した際、ブラウザのオーバーレイではなく「コンテンツのリサイズ」として扱わせる指示。 -
viewport-fit=cover: iPhoneのノッチ(切り欠き)やセーフエリアの全画面描画を許可。
(2) 100vh の追放と 100dvh への移行
古典的な height: 100vh; は、アドレスバーが出ている状態でも「アドレスバーが引っ込んだときの最大高さ」を返してしまうため、最下部が見切れる最大の原因でした。
これを、アドレスバーの伸縮にリアルタイム連動する 100dvh(Dynamic Viewport Height) および JavaScriptが計算するカスタムプロパティへと置き換えました。
html, body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden; /* 画面全体のバウンススクロールを防止 */
}
.p2p-standalone-app {
display: flex;
flex-direction: column;
width: 100%;
/* 動的ビューポート高さ(フォールバック付き) */
height: 100dvh;
height: var(--p2p-viewport-height, 100dvh);
overflow: hidden;
}
(3) window.visualViewport API によるリアルタイム追従
iOS Safari など、CSSだけでは完全に追従しきれない環境を完全に制覇するため、VisualViewport API を用いてミリ秒単位で高さを同期します。
/**
* 仮想キーボード連動&ビューポート監視ハンドラ
*/
function setupViewportHandler() {
if (!window.visualViewport) return;
function onViewportChange() {
const height = window.visualViewport.height;
// キーボードの高さを差し引いた可視領域のピクセル数をCSS変数に注入
document.documentElement.style.setProperty('--p2p-viewport-height', `${height}px`);
// 入力欄にフォーカスがある場合、最下部へ自動スクロール
const activeEl = document.activeElement;
if (activeEl && (activeEl.id === 'p2p-message-input' || activeEl.tagName === 'TEXTAREA')) {
setTimeout(() => {
activeEl.scrollIntoView({ block: 'nearest' });
if (typeof scrollToBottom === 'function') {
scrollToBottom();
}
}, 50);
}
}
// キーボードの出現・格納、画面回転を検知
window.visualViewport.addEventListener('resize', onViewportChange);
window.visualViewport.addEventListener('scroll', onViewportChange);
// 初期化時にも一度実行
onViewportChange();
}
(4) iOSの自動ズーム防止とセーフエリア対応
/* iOSのフォーカス時勝手ズームを防止(font-size 16px 以上が必須) */
@media (max-width: 768px) {
.p2p-xchat-text-input,
#p2p-message-input {
font-size: 16px !important;
}
/* iPhone ホームバー(下部インジケーター)との重なり防止 */
.p2p-xchat-input-row {
padding-bottom: max(8px, env(safe-area-inset-bottom));
padding-left: max(8px, env(safe-area-inset-left));
padding-right: max(8px, env(safe-area-inset-right));
}
}
[!IMPORTANT]
iOS Safari は、入力フォームのfont-sizeが 16px 未満 (例えば 14px や 15px)の場合、「文字が小さくて読みにくいだろう」と判断して画面全体を自動で拡大ズームしてしまいます。一度ズームされるとユーザーがピンチインで戻すまでレイアウトが崩壊するため、 モバイルでのfont-size: 16px指定は絶対厳守の鉄則 です。
4. 自動テストと実機での動作検証
専用のテストスクリプトおよび iPhone(iOS 17 Safari)と Android(Pixel / Chrome)の実機で検証を行いました。
| テスト項目 | 従来の挙動 | 今回の改修後 |
|---|---|---|
| 初期表示時の入力バー | アドレスバーに押されて画面下に沈没 | 画面最下部にピタッと収まる |
| キーボード出現時 | 入力欄が背後に隠れて見えなくなる | キーボードの上端にピタッと吸い付く! |
| 文字入力中の画面ズーム | 勝手にズームして左右がハミ出る | 倍率 1.0 倍を完全にキープ |
| iPhone ホームバー操作 | 送信ボタンとホームバーが被って誤爆 | セーフエリア分の余白で快適操作 |
5. まとめと次回予告
モバイルWebにおける最大の難所「ソフトウェアキーボード問題」を、interactive-widget、100dvh、VisualViewport API、そして 16pxルール の連携プレーによって完全制覇しました。
これにより、スマートフォンからでもLINEやDiscordと全く遜色のない、極上のタイピング体験が手に入りました。
しかし、キーボードに追従するようになったことで、今度は別の問題が露呈しました。
「キーボードが開いたとき、画面全体の高さが縮むのはいいんだけど、 チャット部屋のタイトルやホスト名が書かれた上部ヘッダーまで一緒にスクロールして画面外へ逃げてしまう! 」
「今どの部屋で話しているのか分からなくなるし、退出ボタンが押せない!」
次回は、 【第040回】キーボードが出てもヘッダーが逃げない!固定ヘッダーと可変スクロール領域の完全分離 をお届けします。お楽しみに!
本番サービス : https://2ch.biz/