はじめに
フォームを作った。PC の Chrome では問題なく動く。でも iPhone で実機テストをしてみると、入力欄をタップした瞬間に画面がズームして崩れた。
このサイトのハイライト検索コンポーネントを作ったときに実際に発生した問題です。「なぜ Chrome の DevTools シミュレーターでは出なかったのか」「直したつもりなのにまだ崩れる」——そこまでをセットで解説します。
何が起きているか
iOS Safari には、font-size が 16px 未満の <input> をタップすると自動的にズームする仕様があります。アクセシビリティ目的の Safari 独自機能で、小さい文字の入力欄を見やすくするためのものです。
/* これだと iOS Safari でタップ時にズームする */
input {
font-size: 14px;
}
PC デザインに合わせて 14px や 15px に設定しているとこの罠に引っかかります。
なぜ Chrome シミュレーターで再現しないのか
Chrome の DevTools のモバイル表示は「画面サイズと User-Agent を変えた Chrome」です。Safari の独自仕様は再現しません。
この種の問題は実機 iPhone でしか確認できません。「DevTools で確認済み = スマホでも大丈夫」という前提は一度手放す必要があります。フォームを作ったら実機確認を手順に組み込むのが一番の予防策です。
修正方法:モバイル時だけ 16px に上書きする
PC のデザインに合わせて 14px にしたい場合は、メディアクエリでモバイル時だけ上書きします。
input {
font-size: 14px; /* PC はこのまま */
}
@media (max-width: 768px) {
input {
font-size: 16px; /* iOS Safari の自動ズームを止める */
}
}
よくある誤った対処:user-scalable=no
「ズームできないようにすればいい」という発想で user-scalable=no を追加したくなりますが、これはやめてください。
<!-- これは NG です --> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
user-scalable=no はユーザーが意図的にズームする機能を奪います。弱視・老眼の方が「文字を大きくしたい」と思っても操作できなくなります。WCAG(ウェブアクセシビリティガイドライン)でも禁止されている設定です。
問題を「なかったことにする」のではなく、ズームが起きない実装にするのが正しい対処です。
2つのデモで確認する
下の2つのデモは「検索ボックス + 件数バッジ」を横に並べた同じ構成です。iPhone で入力欄をタップして違いを確認してみてください。
Chrome Desktop では font-size の差によるズームは再現しません。input のはみ出し(min-width の差)とレイアウトシフト(display vs visibility の差)はブラウザ幅を狭めると Desktop でも確認できます。
- font-size: 14px(モバイル対応なし)
- 件数: display none → block
- min-width: auto(デフォルト)
- font-size: 16px(@media で上書き)
- 件数: visibility hidden → visible
- min-width: 0
連鎖する罠①:display:none → 表示切り替えでさらにズームする
font-size: 16px に直した。でもまだ iOS でズームする——そういう場合があります。
よくある原因は「隠していた要素を入力フォーカス時に表示する」パターンです。
input.addEventListener('focus', () => {
countBadge.style.display = 'inline'; /* ← これが罠 */
});
display: none だった要素が inline になると、Flexbox のレイアウトが再計算されます。ズーム中の iOS Safari がその変化を「さらに調整が必要」と判断して、追加ズームすることがあります。
対策は visibility: hidden でスペースを確保したままにすることです。
.count-badge {
visibility: hidden; /* display: none の代わりにこちら */
}
.count-badge.visible {
visibility: visible;
}
連鎖する罠②:Flexbox で input がはみ出す
もう一つ。[input][バッジ] を横並びにすると、Flexbox の子要素はデフォルトで min-width: auto(コンテンツが収まる最低限の幅)になります。
隣にバッジや固定幅ボタンが加わると、input が押しつけられてコンテナからはみ出すことがあります。
/* 修正前:input がはみ出す可能性がある */
input {
flex: 1;
/* min-width: auto が暗黙的に適用 */
}
/* 修正後:Flexbox が自由に縮小できるようにする */
input {
flex: 1;
min-width: 0; /* これを追加 */
}
上のデモで「崩れるパターン」の input を選択し、ブラウザ幅を狭めると件数バッジに押されて input がはみ出す挙動を確認できます。
まとめ:3段階の落とし穴
iOS Safari のフォーム自動ズーム問題は、3段階で起きます。
問題1:font-size が 16px 未満
@media (max-width: 768px) で font-size: 16px に上書きする。Chrome シミュレーターでは確認できないため実機必須。
問題2:display:none → block の切り替え
visibility: hidden / visible に変更して、常にスペースを確保する。Flexbox のレイアウト再計算が起きなくなる。
問題3:Flexbox の min-width: auto
横並び Flex レイアウトの input に min-width: 0 を追加する。件数バッジやボタンと並べたときのはみ出しを防ぐ。
3つとも「PC の Chrome では出ない、iPhone で初めて気づく」問題です。フォームを作ったら実機確認を手順に組み込むのが一番の予防策です。
コピペで使える正解パターン
HTML・CSS・JS それぞれの正解パターンをまとめます。コメントを読むだけで「なぜこう書くのか」が分かるようにしました。
HTML
<div class="search-bar"> <!-- flex: 1 + min-width: 0 の入力欄 --> <input type="text" class="search-input" placeholder="キーワード..."> <!-- 件数バッジは display:none ではなく visibility で管理する --> <span class="search-count"></span> </div>
CSS
/* ===== 検索バー Flexbox レイアウト ===== */
.search-bar {
display: flex;
align-items: center;
gap: 8px;
}
/* ===== 入力欄 ===== */
.search-input {
flex: 1;
/* 【重要】Flexbox の min-width はデフォルトで auto。
隣に固定幅の要素があると input が押しつけられてはみ出す原因になる。
0 を指定することで Flexbox が自由に縮小できるようになる。 */
min-width: 0;
font-size: 14px; /* PC 用の見た目に合わせたサイズ */
}
/* 【重要】iOS Safari のフォーム自動ズーム防止。
font-size が 16px 未満の input をタップすると Safari は自動ズームする。
Chrome DevTools のモバイルシミュレーターでは再現しないため
必ず実機 iPhone で確認すること。
NG: user-scalable=no で対処するのは WCAG 違反(ユーザーのズーム操作を奪う)。 */
@media (max-width: 768px) {
.search-input {
font-size: 16px;
}
}
/* ===== 件数バッジ ===== */
.search-count {
white-space: nowrap;
width: 70px; /* 幅を固定しておくと「○件ヒット」の文字数変化でレイアウトが動かない */
/* 【重要】display: none ではなく visibility を使う。
display: none にすると表示時に Flexbox のレイアウトが再計算される。
ズーム中の iOS Safari がその変化に反応してさらにズームすることがある。
visibility: hidden ならスペースを確保したまま非表示になるため
レイアウト変化が起きず、追加ズームを防げる。 */
visibility: hidden;
}
.search-count.is-visible {
visibility: visible;
}
JS
var input = document.querySelector('.search-input');
var count = document.querySelector('.search-count');
input.addEventListener('input', function() {
if (this.value) {
count.textContent = '3件ヒット'; /* 実際はヒット数を計算して代入する */
/* display ではなく visibility のクラスで管理する
→ レイアウト再計算が起きないため iOS Safari の追加ズームを防げる */
count.classList.add('is-visible');
} else {
count.textContent = '';
count.classList.remove('is-visible');
}
});
最後まで読んでいただきありがとうございました。