fix(css): prevent iOS auto-zoom on input focus (#1167)#1180
fix(css): prevent iOS auto-zoom on input focus (#1167)#1180nesquena-hermes wants to merge 2 commits intomasterfrom
Conversation
…ont-size on touch devices (#1167) iOS Safari and most mobile browsers automatically zoom in when a focused input or select has font-size < 16px, and don't reset zoom after blur. This leaves the viewport zoomed in until the user manually pinches out. The textarea#msg composer was already 16px and unaffected. The inputs that triggered the bug: sidebar search (13px), session rename input (13px), settings selects (12px), and dialog inputs (14px). Fix: one CSS media query targeting touch-primary devices (@media (hover:none) and (pointer:coarse)) that bumps all input/textarea/select to max(16px, 1em). Uses !important to override the per-element font-size rules without touching those rules individually. This keeps desktop layouts unchanged. Closes #1167
) Add a regression test that asserts the global mobile media query bumping input/textarea/select to font-size:max(16px,…) is present, so a future per-element font-size tweak (e.g., adding a new 13px input class) cannot silently re-introduce iOS Safari's auto-zoom-on-focus. The existing test_composer_textarea_font_size_mobile only locks the composer textarea — the global rule that closes #1167 across sidebar search, rename inputs, settings selects, and dialog inputs deserves its own assertion. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
nesquena
left a comment
There was a problem hiding this comment.
Review — end-to-end ✅ (clean approve, regression test pushed)
What this ships
#1167 — iOS Safari auto-zooms when an <input>, <textarea>, or <select> with font-size < 16px is focused, and doesn't un-zoom on blur. Cdaringe's screenshots showed the WebUI getting stuck zoomed-in after tapping any small input (sidebar search, rename, settings selects, dialog inputs).
11-line CSS-only fix at the end of static/style.css:
@media (hover:none) and (pointer:coarse){
input,textarea,select{font-size:max(16px,1em)!important;}
}Traced against upstream hermes-agent
Pure CSS in static/style.css. Zero agent-side coupling.
End-to-end trace
Media query targets touch-primary devices only
(hover:none) and (pointer:coarse) is the canonical "phone or tablet" detection. Won't match:
- Desktop with mouse/trackpad:
hover:hover,pointer:fine→ rule doesn't apply ✅ - Surface / iPad with attached keyboard reporting
pointer:fine→ rule doesn't apply (acceptable trade-off; their keyboards bypass the touch keyboard zoom anyway) ✅
This is the same media query the existing boot.js keydown handler uses for mobile Enter detection (test_mobile_layout.py:255-258) — consistent across the codebase.
max(16px, 1em) semantics
In a font-size declaration, 1em resolves to the parent's computed font-size. So:
- Small input nested inside body (16px default):
max(16px, 16px)= 16px ✅ - Input inside a 14px parent:
max(16px, 14px)= 16px ✅ - Input inside an 18px parent (user OS-level larger fonts):
max(16px, 18px)= 18px ✅ (preserves user accessibility preference)
!important overrides inline styles
index.html:700, 733, 774 have inline style="font-size:13px" (settingsBotName, settingsPassword) and font-size:12px (previewEditArea). CSS specificity rules:
| Rule | Wins over |
|---|---|
Stylesheet !important |
Inline without !important ✅ |
Inline without !important |
Stylesheet without !important |
So font-size:max(16px,1em)!important in the stylesheet correctly overrides the font-size:13px/font-size:12px inline values. Verified by reading specificity, no inline !important in the affected elements.
Affected elements verified
Per the PR table — all the small inputs were below 16px on mobile:
| Element | Pre-fix | Post-fix on mobile |
|---|---|---|
.sidebar-search input |
13px | 16px ✅ |
.session-title-input (rename) |
13px | 16px ✅ |
select (settings dropdowns) |
12px | 16px ✅ |
.app-dialog-input |
14px | 16px ✅ |
.onboarding-field input/select |
13px | 16px ✅ |
textarea#msg (composer) |
16px | 16px (unchanged) ✅ |
previewEditArea textarea |
12px (inline) | 16px ✅ |
settingsBotName / settingsPassword |
13px (inline) | 16px ✅ |
What I pushed — a5ad154
Added test_touch_device_inputs_meet_zoom_threshold to tests/test_mobile_layout.py which asserts the new media query exists with the right shape:
pattern = re.compile(
r'@media\s*\(hover:none\)\s*and\s*\(pointer:coarse\)\s*\{[^}]*'
r'input\s*,\s*textarea\s*,\s*select\s*\{[^}]*'
r'font-size:\s*max\(\s*16px',
re.DOTALL,
)This locks the bug from regressing if a future per-element font-size tweak forgets to consider the touch-primary case. The existing test_composer_textarea_font_size_mobile only covers the composer; this new assertion covers the global rule across sidebar/select/dialog/onboarding fields.
CI re-ran green on a5ad154: 3.11 ✅, 3.12 ✅, 3.13 ✅.
Edge-case trace
| Scenario | Expected | Actual |
|---|---|---|
| Desktop browser at any width | unchanged (no rule applies) | ✅ media query gated by hover:none + pointer:coarse |
| Tap small sidebar search on iPhone | input shows at 16px, no auto-zoom | ✅ |
| Settings dropdown on iPhone | select trigger at 16px, no auto-zoom | ✅ (native select dropdown UI is OS-rendered, but the trigger text controls zoom) |
| User has OS large-text setting (1em > 16px) | input renders at 1em, still no zoom | ✅ max(16px, 1em) |
Inline-styled input (style="font-size:13px") |
overridden to 16px on mobile | ✅ stylesheet !important > inline |
| Future per-element 13px override | still overridden to 16px on mobile | ✅ !important wins regardless of source order |
| iPad with attached keyboard reporting fine pointer | rule may not apply (acceptable; physical keyboard doesn't trigger zoom) | ✅ as designed |
Tests
- My new test (
test_touch_device_inputs_meet_zoom_threshold): asserts the media query and rule shape. Passes. - Existing 24 mobile layout tests: all pass (including the existing 16px composer assertion).
- Local full suite: 2596 passed, 47 skipped, 1 PR-unrelated pre-existing failure (macOS-only
test_sprint3quirk). - CI on PR after my push: ✅ test (3.11), ✅ test (3.12), ✅ test (3.13).
Other audit — confirmed correct
- CSS placement: at end of
style.cssso the rule isn't shadowed by later more-specific rules — though!importantwould win regardless. - No regression risk on desktop: the media query gate is robust; the rule simply doesn't apply.
- No layout flicker:
max(16px, 1em)evaluates statically when the touch viewport breakpoint applies; no JS runtime cost.
Minor observations (non-blocking)
- The fix bumps font-size globally on mobile. Some tightly-spaced inputs (e.g. settings dropdowns sized for 12px) may now wrap or push other content slightly. This is the price of stopping auto-zoom; the issue explicitly calls out the trade-off as acceptable.
- iOS Safari select dropdowns use OS-rendered native UI for the option list, so the rule's effect is on the trigger text only. Still sufficient — zoom-on-focus triggers off the trigger element's font-size.
!importantis generally a code smell, but it's the right tool for a viewport-level zoom workaround that needs to override every existing per-element font-size declaration without rewriting them all.
Recommendation
Approved. Surgical 11-line CSS fix that closes #1167 with the canonical touch-primary media query. max(16px, 1em) correctly handles the user-accessibility-larger-fonts case. !important is justified here as the safe global override. No JS, no agent coupling, no desktop regression. Pushed a small regression test that locks the rule's shape so future restructuring can't silently break it. Parked at approval — ready for the release agent's merge/tag pipeline.
) Merged as v0.50.229. 2678 tests passing. Browser QA 21/21. All three PRs were independently reviewed and approved by @nesquena with reviewer commits pulled in: - #1181 (#1158): `d974388` (stale-response race in _loadOlderMessages) - #1182: `7e20006` (full-scan fallback path consistency) - #1180: `a5ad154` (regression test for iOS zoom threshold) Thanks @jasonjcwu (#1158)!
|
Merged as v0.50.229 via #1183. Thank you nesquena-hermes! |
…squena#1183) Merged as v0.50.229. 2678 tests passing. Browser QA 21/21. All three PRs were independently reviewed and approved by @nesquena with reviewer commits pulled in: - nesquena#1181 (nesquena#1158): `d974388` (stale-response race in _loadOlderMessages) - nesquena#1182: `7e20006` (full-scan fallback path consistency) - nesquena#1180: `a5ad154` (regression test for iOS zoom threshold) Thanks @jasonjcwu (nesquena#1158)!
Summary
Fixes the mobile viewport zoom issue where tapping any input field (sidebar search, session rename, settings fields, selects) causes iOS Safari to zoom in and not reset after blur.
Root cause
iOS Safari automatically zooms in when a focused
<input>,<textarea>, or<select>hasfont-size < 16px. It doesn't un-zoom after blur. Thetextarea#msgcomposer was already16pxand unaffected. The triggering elements were:.sidebar-search input.session-title-input(rename)select(settings dropdowns).app-dialog-input.onboarding-field input/selectFix
One CSS media query at the end of
style.csstargeting touch-primary devices:(hover:none) and (pointer:coarse)is the canonical way to target touch devices (phones, tablets) without affecting desktop browsers at any viewport width.max(16px, 1em)ensures the floor is met while respecting any larger user-configured font size.Testing
Closes #1167