From a5be07ac0fa8683f48705e2e3c4d811bb4f17745 Mon Sep 17 00:00:00 2001 From: Tudor Sitaru Date: Mon, 18 May 2026 15:38:28 +0100 Subject: [PATCH] fix(nav): keep bottom tab bar flush to visible viewport on iOS Chrome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iOS Chrome (and some Android browsers) auto-hide their URL bar on scroll. This grows the visual viewport without changing the layout viewport, so a position:fixed bar pinned to bottom:0 — which is relative to the layout viewport — appears to float mid-screen with a gap beneath it. Safari masks the bug because its toolbar shrinks rather than fully retracting. Track the delta between the visual and layout viewports via the VisualViewport API and write it to a --mobile-bar-offset CSS var. The bar uses translate3d to apply that offset, which both fixes the gap and enables hardware compositing so it tracks the toolbar animation without flicker. Co-Authored-By: Claude Opus 4.7 --- nextjs-app/components/Navigation.module.css | 5 ++++ nextjs-app/components/Navigation.tsx | 28 +++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/nextjs-app/components/Navigation.module.css b/nextjs-app/components/Navigation.module.css index d4e4b15..306091f 100644 --- a/nextjs-app/components/Navigation.module.css +++ b/nextjs-app/components/Navigation.module.css @@ -233,5 +233,10 @@ box-shadow: 0 -2px 12px rgba(26, 22, 18, 0.06); /* Respect iPhone home-indicator inset */ padding-bottom: env(safe-area-inset-bottom, 0); + /* Compensate for iOS Chrome's auto-hiding URL bar — Navigation.tsx + writes the offset based on the Visual Viewport API. translate3d + (instead of translateY) forces hardware compositing so the bar + doesn't lag/flicker during the toolbar animation. */ + transform: translate3d(0, var(--mobile-bar-offset, 0px), 0); } } diff --git a/nextjs-app/components/Navigation.tsx b/nextjs-app/components/Navigation.tsx index 73c38a8..24bbdc8 100644 --- a/nextjs-app/components/Navigation.tsx +++ b/nextjs-app/components/Navigation.tsx @@ -5,6 +5,7 @@ 'use client'; +import { useEffect } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { useComparison } from '@/hooks/useComparison'; @@ -50,6 +51,33 @@ export function Navigation() { return pathname.startsWith(path); }; + /** + * iOS Chrome (and some Android browsers) auto-hide their URL bar on scroll, + * which grows the visual viewport without changing the layout viewport. + * `position: fixed; bottom: 0` sticks to the layout viewport, so our tab + * bar appears to float mid-screen with a gap beneath it. Track the delta + * via VisualViewport and apply it as a translate so the bar always sits + * flush against the visible bottom edge. + */ + useEffect(() => { + const vv = window.visualViewport; + if (!vv) return; + const root = document.documentElement; + const update = () => { + const offset = window.innerHeight - (vv.height + vv.offsetTop); + // Only positive offsets are meaningful (bar hidden → push down). + root.style.setProperty('--mobile-bar-offset', `${Math.max(0, offset)}px`); + }; + update(); + vv.addEventListener('resize', update); + vv.addEventListener('scroll', update); + return () => { + vv.removeEventListener('resize', update); + vv.removeEventListener('scroll', update); + root.style.removeProperty('--mobile-bar-offset'); + }; + }, []); + const items = [ { href: '/', label: 'Search', Icon: SearchIcon }, { href: '/compare', label: 'Compare', Icon: CompareIcon },