fix: keep iOS UI inside the visual viewport#19
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Improve mobile (notably iOS Safari) layout stability by preventing horizontal clipping/overflow and handling dynamic viewport height/safe-area insets.
Changes:
- Add safe-area–aware padding/inset rules and width constraints for the composer on mobile.
- Prevent status bar content from pushing right-side controls off-screen on narrow viewports.
- Improve viewport height handling (dvh + JS sync) and attempt to neutralize iOS “stray scroll” during keyboard focus.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/styles/statusBar.css | Hide overflow in the status bar to keep right-side buttons visible on narrow screens. |
| src/styles/responsive.css | Add iOS safe-area padding, constrain composer widths, and hide long status path on small screens. |
| src/styles/base.css | Prefer 100dvh when supported and add overscroll-behavior to reduce iOS scroll issues. |
| src/app/elements.ts | Reset window scroll to (0,0) during app-height sync to counter iOS keyboard/focus scroll. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /* Prevent right-side buttons (settings, bucket, …) from being pushed | ||
| off-screen on narrow viewports if a child refuses to shrink. */ | ||
| overflow: hidden; |
| /* Defensive: never let the composer (or its footer) overflow the | ||
| viewport horizontally on iOS, where keyboard / focus-scroll quirks | ||
| can otherwise clip the send button at the right edge. */ | ||
| max-width: calc(100vw - 20px); |
| /* iOS Safari sometimes scrolls the html element when an input is focused | ||
| even with body { overflow: hidden }. Lock scroll position explicitly. */ | ||
| overscroll-behavior: none; |
| // iOS Safari can scroll the document body when focusing an input near the | ||
| // bottom of the viewport (keyboard appearance). With our fixed-height, | ||
| // overflow:hidden layout that just leaves part of the UI — e.g. the | ||
| // composer footer or send button — visually clipped at the edges of the | ||
| // visual viewport. Snap any stray scroll back to the top. | ||
| if (window.scrollY !== 0 || window.scrollX !== 0) { | ||
| window.scrollTo(0, 0); | ||
| } |
Fixes a few mobile/iOS layout problems visible on iPhone Safari: - Status bar's right-most buttons (session bucket flag, settings gear) could be pushed off-screen on narrow viewports because the cwd path refused to shrink enough. Hide `.statusPath` on `max-width: 700px`, cap the title width, and add `overflow: hidden` to `.statusBar` as a defensive guard so a stubborn child can't punt the right side of the header off-screen. - Composer footer (and the send button in particular) could be clipped at the right edge on iOS. This happens when iOS Safari programmatically scrolls the document while focusing the textarea, even though body has `overflow: hidden`. Snap any stray window scroll back to (0,0) inside `syncAppHeight`, and constrain the composer / footer to `calc(100vw - 20px)` on mobile so it physically can't overflow. - Add `padding-top: max(10px, env(safe-area-inset-top))` to the mobile `.app` so the status bar is not flush with the iOS notch / dynamic island in standalone (Add to Home Screen) mode. Apply the same safe- area treatment to the expanded composer's `inset`. - Use `100dvh` for `--app-height` where supported, with the existing JS visualViewport sync still acting as the source of truth at runtime. This avoids a brief mis-sized layout on first paint before the JS height sync runs. - Add `overscroll-behavior: none` to html/body so iOS rubber-band scrolling doesn't reveal blank chrome behind the app. No tests added — this is purely CSS / layout glue. `npm run typecheck`, `npm run test:unit`, and `npm run build` all pass. Signed-off-by: Megha Goyal <goyamegh@amazon.com>
…mposer
Follow-up to bfaa2b1 ("fix: keep iOS UI inside the visual viewport"):
the previous round addressed layout overflow and visual-viewport sync,
but on real iPhones (observed iPhone 16 Pro Max / iOS 17 Safari) the
composer was still being clipped at the right edge with the send button
half cut off, and the session bar tabs were overflowing horizontally.
Two iOS-specific behaviours were the actual root cause:
1. iOS Safari auto-zooms the page on input focus whenever the focused
input/textarea has font-size < 16px. The composer textarea inherits
the body's 15px font, so every tap on the composer triggered a small
automatic zoom that pushed the send button off-screen and made the
session-bar tabs wider than the visual viewport. The user-visible
symptom was "the UI is a little zoomed in".
Fix: at @media (max-width: 700px), pin every text input
(textarea / input[type=text|search|...] / bare input) to
font-size: 16px. iOS only auto-zooms below 16px, so this fully
disables the behaviour without any JS or zoom-disabling viewport
meta-tag (which would harm accessibility / WCAG 1.4.4).
2. iOS Safari can still scroll the document body programmatically to
keep the caret of a focused textarea in view, even with
`html, body { overflow: hidden }`. That scroll fires window's
`scroll` event, NOT visualViewport's, so the existing snap-back
inside syncAppHeight (which only listens to visualViewport
resize/scroll and window resize) never ran.
Fix:
- Add a passive window-level scroll listener that runs
syncAppHeight, which already snaps `window.scrollX/Y` back to 0.
- On focusin, schedule one extra rAF-deferred snap-back so the UI
can never land in a half-scrolled state on the next frame.
- Opt into `overflow: clip` on html/body via @supports for
Safari 16+ / Chrome 90+ / Firefox 81+. `clip` is stricter than
`hidden`: it forms no scroll container at all, so iOS's internal
caret-tracking scrollIntoView has nothing to scroll.
Verified locally:
- npm run typecheck — clean
- npm run build — succeeds
- npm run test:unit — 55/55 pass
Real-iPhone manual verification is the missing piece tracked in
ashwin-pc#23; this commit is the proposed fix for that issue.
Signed-off-by: Megha Goyal <goyamegh@amazon.com>
a874b90 to
4fcf435
Compare
|
Pushed a follow-up commit ( The previous version of this PR was working in Chromium iPhone-emulation but on a real iPhone 16 Pro Max (iOS 17 Safari) the composer's send button was still being clipped at the right edge and the session-bar tabs were overflowing horizontally — captured in #23 with screenshots. Two iOS-specific behaviours were the actual root cause; the new commit addresses both:
Diff (on top of the earlier PR commit): Verified locally:
Closes #23. |
What
Fixes a few mobile / iOS layout problems visible on iPhone Safari.
Issues fixed
1. Status-bar buttons clipped on the right
On narrow iPhone viewports the cwd path in the status bar refused to shrink enough, which pushed the right-most icon buttons (session bucket flag and settings gear) off-screen.
.statusPathonmax-width: 700px(the working directory is still surfaced via the empty-state chooser and the session list, so no information is lost on mobile)..statusTitleatmin(60vw, 240px)on mobile.overflow: hiddento.statusBaras a defensive guard so a stubborn child can never punt the right side of the header off-screen.2. Composer / send button clipped on the right
The send button could be visually cut off at the right edge on iOS. iOS Safari programmatically scrolls the document while focusing inputs near the bottom of the viewport, even though
body { overflow: hidden }. That left part of the composer footer (and especially the send button) outside the visual viewport.syncAppHeight, snap any straywindow.scrollX/scrollYback to(0, 0)whenever the visual viewport changes (resize / scroll)..composertomax-width: calc(100vw - 20px)and explicitly addmin-width: 0/max-width: 100%to.composerFooterso it physically cannot overflow.3. No top safe-area inset on iOS standalone
In standalone (Add to Home Screen) mode, the status bar was flush against the iOS notch / dynamic island.
padding-top: max(10px, env(safe-area-inset-top))to the mobile.app.env(safe-area-inset-*)values to the expanded composer'sinset.4. Misc viewport hardening
100dvhfor--app-heightwhere supported (the existing JSvisualViewportsync still acts as the source of truth at runtime). Avoids a brief mis-sized layout on first paint before the JS sync runs.overscroll-behavior: nonetohtmlandbodyso iOS rubber-band scrolling doesn't reveal blank chrome behind the app.Files
src/styles/base.css—100dvhfallback,overscroll-behavior: none.src/styles/statusBar.css—overflow: hidden.src/styles/responsive.css— mobile fixes (safe-area, composer max-width, hide path, expanded composer insets).src/app/elements.ts— reset stray window scroll insidesyncAppHeight.Testing
npm run typecheck— passesnpm run test:unit— 50/50 passingnpm run build— succeedsNo new tests added; this PR is purely CSS / layout glue.