iOS の WKWebView でフォームの入力欄をタップすると、画面が勝手に拡大してわずかに右へずれる――.NET MAUI で Android は問題ないのに、iPhone だけ挙動が違う。これはよくある落とし穴です。原因は Safari/WKWebView の自動ズーム仕様。この記事では「なぜ起きるのか」「どう直すのか」を、CSS/メタタグ/MAUI カスタムハンドラ/JS 補助まで実装レベルで徹底解説します。
WKWebViewで入力フォーカス時に起こるズーム・位置ずれの正体
iOS の Safari/WKWebView には、アクセシビリティの一環として <input> / <textarea> のフォントサイズが 16px 未満 の場合にフォーカス時(キーボード表示時)に自動的に 1.2~1.5 倍ほど拡大する仕様があります。この「自動ズーム」はユーザーの読みやすさを確保する目的で動作し、ネイティブ側のズーム設定(たとえば ScrollView.ZoomScale)では抑止できません。
自動ズームが発動するとレイアウト計算が再実行され、スクロール位置の再補正も同時に走ります。とくに横方向に余白やマージンがあるフォームでは、視認性確保のために要素が画面内へ「寄せられ」、結果として右にずれたように見えることがあります。Android WebView ではこの自動ズームが基本的に発動しないため、同じ HTML/CSS でも再現しない、という差が生じます。
再現用ミニマルコード
<!doctype html>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { margin: 24px; }
input { font-size: 12px; padding: 10px; width: 100%; }
</style>
<input placeholder="ここをタップすると iOS で拡大">
上記を WKWebView で表示し、iOS 端末でタップすると拡大が起きます。Android では変化しません。
なぜ ScrollView.ZoomScale では止められないのか
MAUI 側のズーム制御は「ピンチ操作での拡大縮小」に対するもので、ブラウザのアクセシビリティによる自動ズームは別経路で実行されます。つまり、iOS のレンダラ内部(レイアウト計算+ビューポート補正)で完結しており、ネイティブ側のズーム禁止設定は影響しません。
解決策(推奨順)
最も効果があり副作用の少ない順に並べます。ほとんどのケースは「① CSS で 16px 以上」にするだけで解決します。
| 手順 | 内容 | 効果 |
|---|---|---|
| ① CSS でフォントサイズを 16px 以上に | input, textarea, select, button { font-size: 16px; } など、フォームコントロールのフォントを 16px 以上に。UI ライブラリを使う場合も最終的にこの条件を満たすよう上書きします。 | iOS の自動ズーム条件を外れるため、拡大も横ずれも発生しない(根本解決)。 |
| ② 追加の保険(必要に応じて) | <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">ピンチズームを無効化して意図しない拡大を防止。 | ユーザーの任意拡大を制限するのでアクセシビリティ要件に注意。 |
| ③ スクロール位置の手動補正 | キーボード表示通知に連動して window.scrollTo() などで位置を戻す。特殊レイアウトや固定ヘッダー等でのみ併用。 | 細かい崩れを最後の手段としてリカバリ。 |
CSS実装例(最小)
/* すべてのフォーム要素のフォントを 16px 以上に */
input, textarea, select, button {
font-size: 16px;
line-height: 1.4;
}
/* プレースホルダーの文字も 16px 以上に統一 */
input::placeholder, textarea::placeholder {
font-size: 16px;
}
/* iOS のシステムフォント&ダイナミックタイプ追従 */
html { font: -apple-system-body; }
アクセシビリティとデザインの両立(どうしても小さく見せたい場合)
「見た目は 14px 相当にしたいが、自動ズームは避けたい」という場合は、フォントは 16px のまま要素全体を CSS で縮小します。
/* スケール縮小:見た目は 0.875 倍(≒14px 相当) */
.control--scaled {
--s: 0.875;
transform: scale(var(--s));
transform-origin: top left;
/* レイアウト幅が縮むので補正(横方向の実効幅を維持) */
width: calc(100% / var(--s));
}
この方法なら iOS の「フォントサイズ検査」は 16px と判断され、自動ズームが発動しません。
MAUI カスタムハンドラ側のポイントと実装例
MAUI のカスタムハンドラで WKWebView を生成する場合、ScrollView.ZoomScale などの設定は自動ズーム抑止に無関係です。代わりにCSS を注入して確実に条件を満たすようにします。
最小ハンドラ(ズーム設定は不要)
public class MyWebViewHandler : ViewHandler<WebView, WKWebView>
{
protected override WKWebView CreatePlatformView()
{
// iOS の自動ズームには影響しないため ZoomScale 設定は不要。
return new WKWebView();
}
}
CSS をドキュメント開始時に自動注入する
ユーザーが任意のページを表示しても必ず 16px 以上になるよう、WKUserScript で CSS を挿入します(メインフレームのみ/ドキュメント開始で注入)。
using Foundation;
using WebKit;
using CoreGraphics;
public class MauiWebViewHandler : ViewHandler
{
protected override WKWebView CreatePlatformView()
{
var config = new WKWebViewConfiguration();
var controller = config.UserContentController ?? new WKUserContentController();
var css = @"
input, textarea, select, button { font-size: 16px !important; line-height: 1.4; }
input::placeholder, textarea::placeholder { font-size: 16px !important; }
html { font: -apple-system-body; }";
var js = $@"(function(){{
var s = document.createElement('style');
s.type = 'text/css';
s.appendChild(document.createTextNode(`{css}`));
document.head.appendChild(s);
}})();";
controller.AddUserScript(new WKUserScript(
new NSString(js),
WKUserScriptInjectionTime.AtDocumentStart,
true // メインフレームのみ
));
config.UserContentController = controller;
var web = new WKWebView(CGRect.Empty, config);
return web;
}
}
キーボード表示時の位置補正(最終手段)
レイアウトが複雑でわずかなズレが残る場合のみ、通知に合わせて微調整します。
using UIKit;
using WebKit;
public partial class MauiWebViewHandler : ViewHandler
{
NSObject? willShow;
NSObject? willChange;
protected override void ConnectHandler(WKWebView platformView)
{
base.ConnectHandler(platformView);
willShow = UIKeyboard.Notifications.ObserveWillShow((s, e) =>
{
// 例:必要に応じてページ先頭へ戻す(本番は対象要素へスクロールする等)
platformView.EvaluateJavaScript("window.scrollTo(0, 0);", null);
});
willChange = UIKeyboard.Notifications.ObserveWillChangeFrame((s, e) =>
{
// フレーム変化中の細かい補正を入れる場合はここで JS を投げる
});
}
protected override void DisconnectHandler(WKWebView platformView)
{
willShow?.Dispose();
willChange?.Dispose();
base.DisconnectHandler(platformView);
}
}
JS 側の補助(VisualViewport を活用)
(function () {
if (!('visualViewport' in window)) return;
function keepFocusedVisible() {
const el = document.activeElement;
if (!el) return;
// 固定ヘッダーなどがある場合は scroll-margin-top を CSS 側で指定しておく
el.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'auto' });
}
visualViewport.addEventListener('resize', keepFocusedVisible);
visualViewport.addEventListener('scroll', keepFocusedVisible);
})();
ビューポート高さがキーボードで縮むとき、フォーカス要素が隠れないよう中央へ寄せるだけの軽い補助です。
meta viewport の効果と注意点
user-scalable=no や maximum-scale=1 を指定すると、ユーザーのピンチズームやダブルタップズームを無効化できます。意図しない拡大を徹底的に防げる反面、弱視のユーザーに対して拡大手段を奪うことになるため、組織のアクセシビリティ指針・法令準拠(WCAG など)を満たす必要がある場合は慎重に判断してください。最優先はあくまで「フォントサイズ 16px 以上」です。
<meta name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
Android で問題が出にくい理由
Android WebView には iOS のような「フォーカス時の自動ズーム」仕様が基本的にありません。したがって、同じフォームでも Android では拡大や横ずれが起こらず、iOS のみで再現します。クロスプラットフォームでは「iOS 独自の仕様」を前提に CSS を設計しておくのが重要です。
よくある落とし穴と回避策
| 落とし穴 | 症状 | 回避策 |
|---|---|---|
| UI フレームワーク既定が 14px | iOS でのみ拡大&横ずれ | !important を含む最終レイヤー CSS で 16px 以上を強制 |
rem の基準が 14px | 媒体により自動ズーム発動 | html { font-size: 100%; } とし、フォームは明示で 16px 以上 |
| 固定ヘッダー+内部スクロール | フォーカス時に入力欄が隠れる | :focus { scroll-margin-top: 64px; } で余白を確保 |
| 長いプレースホルダー | 表示中に横方向へ再配置 | プレースホルダーも 16px に、かつ短く簡潔に |
-webkit-text-size-adjust の乱用 | 意図せぬ文字の拡大縮小 | 基本は 100% を維持。自動ズーム対策には直接寄与しない |
| 要素個別に縮小(transform)だけ適用 | 横幅が縮んでクリック領域が狭い | 前述の width: calc(100% / s) 補正を併用 |
実務で使えるスニペット集
フォーム一括ガード(最優先の 16px 対策)
:root { --control-font: 16px; }
input, textarea, select, button {
font-size: var(--control-font);
line-height: 1.5;
min-height: 44px; /* タップ領域の目安 */
-webkit-appearance: none;
appearance: none;
}
input::placeholder, textarea::placeholder { font-size: var(--control-font); }
固定ヘッダーとキーボードの干渉を避ける(CSS+JS)
/* 固定ヘッダーが 56px の場合の一例 */
:focus { scroll-margin-top: 56px; }
html, body {
overscroll-behavior-y: contain; /* iOS のバウンスで意図せぬ位置補正を抑制 */
}
(function () {
if (!window.visualViewport) return;
const root = document.documentElement;
function applyKeyboardInset() {
const inset = Math.max(0, window.innerHeight - visualViewport.height);
root.style.setProperty('--keyboard-inset', inset + 'px');
// 余白が必要なラッパーに padding-bottom: var(--keyboard-inset) を適用
}
visualViewport.addEventListener('resize', applyKeyboardInset);
visualViewport.addEventListener('scroll', applyKeyboardInset);
applyKeyboardInset();
})();
.NET MAUI から任意タイミングで JS を投げる
// 例:ページ読み込み後にフォーカス移動やスクロールを調整
platformView.EvaluateJavaScript(@"
(function () {
var first = document.querySelector('input, textarea');
if (first) { first.blur(); window.scrollTo(0, 0); }
})();
", null);
メリット・デメリット比較(総括)
| 対策 | メリット | デメリット |
|---|---|---|
| フォントサイズを 16px 以上 | 実装が簡単・確実/iOS・Android で一貫 | 極小文字のデザインがしづらい |
| meta viewport でズーム禁止 | 画面レイアウトを完全固定できる | ユーザー拡大を制限しアクセシビリティ低下の可能性 |
| JS で位置補正 | 細かな崩れに柔軟に対応可 | 実装コスト/端末差異追随のメンテ工数 |
チェックリスト(導入前・導入後)
- フォームの
input/textarea/select/buttonのフォントが最終的に 16px 以上になっているか(ブラウザの開発者ツールで確認)。 - プレースホルダー・エラーメッセージ・サジェスト候補など間接的に表示される文字も 16px 以上か。
- 固定ヘッダー/タブバーがある場合、
scroll-margin-topやパディングで隠れないか。 - iPhone の縦横・各世代・iPad でも挙動が安定しているか(Split View も含む)。
- ユーザーの文字サイズ(ダイナミックタイプ)を大きくしたときに UI が破綻しないか(
-apple-system-bodyを活用)。 - アクセシビリティ方針上、
user-scalable=noを使ってよいか(必要に応じて代替手段を提示)。
トラブルシューティング Q&A
Q. 16px にしたのに、まだ少しズレます。 A. 固定要素(ヘッダー/フッター)や内部スクロールの重なりが原因の場合があります。scroll-margin-top の調整、visualViewport の補助スクリプト、あるいはキーボード通知に応じた微調整を併用してください。
<dt>Q. どうしても 14px の見た目にしたい。</dt>
<dd>A. フォント自体は 16px に保ったまま、要素を <code>transform: scale()</code> で縮小し、横幅は <code>width: calc(100% / s)</code> で補正します。クリック領域が狭くならないよう <code>min-height</code> も確保しましょう。</dd>
<dt>Q. <code>-webkit-text-size-adjust</code> を弄れば直りますか?</dt>
<dd>A. 自動ズームへの直接の対策にはなりません。iOS のフォーカス時自動ズームはテキストサイズ調整と別系統です。基本は 16px 以上にするのが確実です。</dd>
<dt>Q. <code>maximum-scale=1</code> は必須ですか?</dt>
<dd>A. 必須ではありません。まずは 16px 以上で様子を見て、どうしても想定外の拡大が残る場合にのみ検討してください。</dd>
実運用での設計指針
- 最優先は根本原因の除去(16px 以上)。UI ライブラリのトークン/テーマに「フォーム最小フォントサイズ 16px」を組み込み、チーム全体で再発を防ぎます。
- ビューポート固定(ズーム禁止)は要件・合意が取れた場合のみに限定。代替として「拡大モード」の別画面を用意する選択肢もあります。
- どうしても残るズレは
visualViewport監視やキーボード通知で最小限の補正だけを入れる。過度な JS 介入は将来の iOS 変更でリスクになります。 - UI の触りやすさは
min-height: 44pxを基準に。誤タップを減らし、入力完了率が上がります。
まとめ
iOS の WKWebView で「入力時に勝手に拡大して横にずれる」問題は、ほぼ例外なくフォームのフォントが 16px 未満であることが原因です。解決のファーストステップは CSS で 16px 以上に揃えること。必要に応じて meta viewport を保険として追加し、それでも残るケースだけ JS/ネイティブ連携で微調整する。この順序で取り組めば、.NET MAUI でも iOS/Android 間で安定したフォーム体験を提供できます。

コメント