From 9681dddc2e09c78361c9cb994c82fa4f02892471 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Wed, 15 Apr 2026 22:11:59 +0800 Subject: [PATCH] Fix iOS Safari zoom-in on lock/unlock Adds a tiny client component mounted in the root layout that listens for pageshow with persisted=true (bfcache resume) and nudges scroll + forces a reflow. Resolves the zoom-desync bug where viewportFit=cover + maximumScale=1 leaves the visual viewport stale after device lock. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/layout.tsx | 2 ++ components/ViewportRedrawOnResume.tsx | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 components/ViewportRedrawOnResume.tsx diff --git a/app/layout.tsx b/app/layout.tsx index e5930fd..db45356 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next"; import { Geist } from "next/font/google"; import { Header } from "@/components/Header"; import { BottomNav } from "@/components/BottomNav"; +import { ViewportRedrawOnResume } from "@/components/ViewportRedrawOnResume"; import "./globals.css"; const geistSans = Geist({ @@ -48,6 +49,7 @@ export default function RootLayout({ suppressHydrationWarning > +
{children}
diff --git a/components/ViewportRedrawOnResume.tsx b/components/ViewportRedrawOnResume.tsx new file mode 100644 index 0000000..a9bb6ee --- /dev/null +++ b/components/ViewportRedrawOnResume.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useEffect } from "react"; + +// iOS Safari bug: with viewportFit=cover + maximumScale=1, restoring from +// bfcache after lock/unlock can leave the visual viewport in a stale +// "zoomed" state. Nudging scroll + forcing a reflow on pageshow +// (persisted) realigns it without changing the zoom level. +export function ViewportRedrawOnResume() { + useEffect(() => { + const onShow = (e: PageTransitionEvent) => { + if (!e.persisted) return; + window.scrollTo(window.scrollX, window.scrollY); + void document.body.offsetHeight; + }; + window.addEventListener("pageshow", onShow); + return () => window.removeEventListener("pageshow", onShow); + }, []); + return null; +}