iOS Safari のフォーム自動ズームを止める方法。
font-size: 16px と3段階の落とし穴

はじめに

フォームを作った。PC の Chrome では問題なく動く。でも iPhone で実機テストをしてみると、入力欄をタップした瞬間に画面がズームして崩れた。

このサイトのハイライト検索コンポーネントを作ったときに実際に発生した問題です。「なぜ Chrome の DevTools シミュレーターでは出なかったのか」「直したつもりなのにまだ崩れる」——そこまでをセットで解説します。

何が起きているか

iOS Safari には、font-size が 16px 未満の <input> をタップすると自動的にズームする仕様があります。アクセシビリティ目的の Safari 独自機能で、小さい文字の入力欄を見やすくするためのものです。

/* これだと iOS Safari でタップ時にズームする */
input {
  font-size: 14px;
}

PC デザインに合わせて 14px15px に設定しているとこの罠に引っかかります。

なぜ 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');
  }
});

最後まで読んでいただきありがとうございました。