Skip to content

fix: keep iOS UI inside the visual viewport#19

Open
goyamegh wants to merge 2 commits into
ashwin-pc:mainfrom
goyamegh:fix/ios-ui-overflow
Open

fix: keep iOS UI inside the visual viewport#19
goyamegh wants to merge 2 commits into
ashwin-pc:mainfrom
goyamegh:fix/ios-ui-overflow

Conversation

@goyamegh

@goyamegh goyamegh commented Jun 7, 2026

Copy link
Copy Markdown

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.

  • Hide .statusPath on max-width: 700px (the working directory is still surfaced via the empty-state chooser and the session list, so no information is lost on mobile).
  • Cap .statusTitle at min(60vw, 240px) on mobile.
  • Add overflow: hidden to .statusBar as 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.

  • Inside syncAppHeight, snap any stray window.scrollX/scrollY back to (0, 0) whenever the visual viewport changes (resize / scroll).
  • On mobile, constrain .composer to max-width: calc(100vw - 20px) and explicitly add min-width: 0 / max-width: 100% to .composerFooter so 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.

  • Add padding-top: max(10px, env(safe-area-inset-top)) to the mobile .app.
  • Apply matching env(safe-area-inset-*) values to the expanded composer's inset.

4. Misc viewport hardening

  • Use 100dvh for --app-height where supported (the existing JS visualViewport sync still acts as the source of truth at runtime). Avoids a brief mis-sized layout on first paint before the JS sync runs.
  • Add overscroll-behavior: none to html and body so iOS rubber-band scrolling doesn't reveal blank chrome behind the app.

Files

  • src/styles/base.css100dvh fallback, overscroll-behavior: none.
  • src/styles/statusBar.cssoverflow: 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 inside syncAppHeight.

Testing

  • npm run typecheck — passes
  • npm run test:unit — 50/50 passing
  • npm run build — succeeds

No new tests added; this PR is purely CSS / layout glue.

Copilot AI review requested due to automatic review settings June 7, 2026 07:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread src/styles/statusBar.css
Comment on lines +10 to +12
/* Prevent right-side buttons (settings, bucket, …) from being pushed
off-screen on narrow viewports if a child refuses to shrink. */
overflow: hidden;
Comment thread src/styles/responsive.css
/* 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);
Comment thread src/styles/base.css
Comment on lines +27 to +29
/* iOS Safari sometimes scrolls the html element when an input is focused
even with body { overflow: hidden }. Lock scroll position explicitly. */
overscroll-behavior: none;
Comment thread src/app/elements.ts
Comment on lines +128 to +135
// 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);
}
goyamegh added 2 commits June 10, 2026 18:25
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>
@goyamegh goyamegh force-pushed the fix/ios-ui-overflow branch from a874b90 to 4fcf435 Compare June 10, 2026 18:29
@goyamegh

Copy link
Copy Markdown
Author

Pushed a follow-up commit (4fcf435) and rebased onto current origin/main (13eb1a4).

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:

  1. iOS Safari auto-zoom on input focus. When a focused <input> / <textarea> has font-size < 16px, iOS Safari programmatically zooms the page on focus. The composer textarea inherits the body's 15px, so every tap on it triggered a small auto-zoom that pushed the send button off-screen and made the session-bar tabs wider than the visual viewport. Symptom users describe as "the UI is a little zoomed in".

    Fix: at @media (max-width: 700px), pin every text-input variant (textarea, input[type=text|search|email|url|tel|password], bare input) to font-size: 16px. iOS only auto-zooms below 16px, so this fully disables the behaviour without touching the viewport meta-tag (no maximum-scale=1 — pinch-zoom remains available, WCAG 1.4.4 friendly).

  2. iOS scrolls the document body to track the caret even with overflow: hidden. That scroll fires window's scroll event, not visualViewport's, so the existing snap-back inside syncAppHeight never ran. Added in this commit:

    • Passive window-level scroll listener that re-runs syncAppHeight (which already snaps window.scrollX/Y back to 0).
    • focusin listener that schedules one extra requestAnimationFrame-deferred snap-back, so the UI can't land in a half-scrolled state on the next frame after focus.
    • @supports (overflow: clip) { html, body { overflow: clip } } 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. overflow: hidden stays as the fallback for older engines.

Diff (on top of the earlier PR commit):

 src/app/elements.ts        | 11 +++++++++++
 src/styles/base.css        |  8 ++++++++
 src/styles/responsive.css  | 16 ++++++++++++++++

Verified locally:

  • npm run typecheck — clean
  • npm run build — succeeds
  • npm run test:unit — 62/62 pass (8 files, including the new tests/focus.test.ts from feat(sessions): pin folders to the top of the session drawer #21)
  • Real-iPhone manual repro — to be re-confirmed; the previous push failed because Safari's service worker cached the old bundle (force-reload required between tests).

Closes #23.

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.

2 participants