From 27cb34df2743ca7e31e95aed919e95f81fa21705 Mon Sep 17 00:00:00 2001 From: neodk2004 Date: Sun, 16 Aug 2026 17:09:50 +0200 Subject: [PATCH] fix(stories): portal the story overlay to escape overflow-auto ancestor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real root cause of the mobile rendering bug (confirmed via an actual Android Chrome screenshot, not devtools emulation): StoryViewer's fixed inset-0 div is a descendant of
in the (app) layout. On Android Chrome, a position:fixed element inside an overflow-auto ancestor can size itself against that ancestor's full scrollable content instead of the real viewport — the overlay rendered many times taller than the screen, so only a small top slice was visible, looking like a heavily zoomed crop. The previous two commits were chasing the video's object-fit/aspect-ratio, which was never the actual problem. Renders the overlay via createPortal to document.body instead, the same pattern Radix/Base UI's Sheet and Dialog already use internally for exactly this class of bug. --- src/components/stories/StoryViewer.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/components/stories/StoryViewer.tsx b/src/components/stories/StoryViewer.tsx index 61b1493..6831746 100644 --- a/src/components/stories/StoryViewer.tsx +++ b/src/components/stories/StoryViewer.tsx @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect, useCallback, useRef } from "react"; +import { createPortal } from "react-dom"; import dynamic from "next/dynamic"; import { X, Eye, Volume2, VolumeX, Loader2 } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -65,6 +66,12 @@ export function StoryViewer({ const [isMuted, setIsMuted] = useState(true); const advancedRef = useRef(false); + // Portal target must wait for mount (document isn't available during SSR). + const [mounted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + const { data: stories = [] } = useQuery( trpc.stories.listActive.queryOptions({ petId }) ); @@ -183,8 +190,14 @@ export function StoryViewer({ return ( <> - {/* Full-screen overlay */} - {open && ( + {/* Full-screen overlay — portaled to document.body so `position: fixed` + escapes the (app) layout's
ancestor. + A fixed descendant of an overflow-auto container can, on some mobile + browsers (confirmed on Android Chrome), size itself against that + container's full scrollable content instead of the real viewport — + the story then renders many times taller than the screen and only + a small top slice is visible, looking like a heavily zoomed crop. */} + {open && mounted && createPortal(
{viewers.length} )} -
+ , + document.body )} {/* Viewer list sheet — owner only */}