Skip to content

fix(css): prevent iOS auto-zoom on input focus (#1167)#1180

Closed
nesquena-hermes wants to merge 2 commits intomasterfrom
fix/1167-mobile-input-zoom
Closed

fix(css): prevent iOS auto-zoom on input focus (#1167)#1180
nesquena-hermes wants to merge 2 commits intomasterfrom
fix/1167-mobile-input-zoom

Conversation

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

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> has font-size < 16px. It doesn't un-zoom after blur. The textarea#msg composer was already 16px and unaffected. The triggering elements were:

Element Current font-size
.sidebar-search input 13px
.session-title-input (rename) 13px
select (settings dropdowns) 12px
.app-dialog-input 14px
.onboarding-field input/select 13px

Fix

One CSS media query at the end of style.css targeting touch-primary devices:

@media (hover:none) and (pointer:coarse) {
  input, textarea, select { font-size: max(16px, 1em) !important; }
}

(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

  • 2644 tests passing.
  • Manual: tap sidebar search on iPhone → no zoom. Tap rename input → no zoom. Tap settings select → no zoom.

Closes #1167

…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]>
Copy link
Copy Markdown
Owner

@nesquena nesquena left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_sprint3 quirk).
  • 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.css so the rule isn't shadowed by later more-specific rules — though !important would 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.
  • !important is 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.

nesquena-hermes added a commit that referenced this pull request Apr 27, 2026
)

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)!
@nesquena-hermes
Copy link
Copy Markdown
Collaborator Author

Merged as v0.50.229 via #1183. Thank you nesquena-hermes!

@nesquena-hermes nesquena-hermes deleted the fix/1167-mobile-input-zoom branch April 27, 2026 23:59
JKJameson pushed a commit to JKJameson/hermes-webui that referenced this pull request Apr 29, 2026
…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)!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mobile viewport handling zoom issues

2 participants