Under the glass
In iOS Safari 26 the toolbar floats over the page as a sheet of glass, and the status bar sits above it. On a full-screen hero you want your image to run under both. Even mymind.com only gets halfway: its page runs under the toolbar, but at the top of the page the status bar stays solid until you scroll. And a fixed hero doesn't get that far: Safari paints both bars a flat colour and the hero stops short.
Here's what I found about how Safari decides, and a recipe that works.
How Safari decides
- Fixed elements never go under the bars. Safari stops anything with
position: fixedat both edges, whatever its height. - But a fixed element can tint them. If it touches a bar and its edge is one solid colour, Safari fills the bar with that colour. A strip of about 6px is enough; 1px isn't.
- The bottom bar shows real content that runs past the bottom of the screen in normal flow, even on a page that can't scroll.
- The top bar only shows content once the page is scrolled. At the very top it's a solid colour. Scrolled, even by code and even with scrolling locked, the content above shows through, blurred.
So the trick is to make the page a little taller than the screen at both ends, and quietly scroll it to the middle.
The recipe
- Set
viewport-fit=cover. In Next.js:export const viewport = { viewportFit: "cover" }. - Keep the hero in normal flow, not fixed, and make it taller than the screen:
calc(TOP + 100dvh + BOTTOM), with TOP at 100px and BOTTOM at 160px, more than any status bar or toolbar. - On mount, lock scrolling and scroll down by TOP. Re-pin on resize; restore both on unmount.
- Put the real interface in a band inside it:
top: TOP, heightcalc(100% - TOP - BOTTOM). Let the background fill everything, overhangs included. That's what shows through. - Keep fixed elements away from the top edge. They make Safari paint the bar solid again.
const TOP = 100const BOTTOM = 160useEffect(() => { const html = document.documentElement const body = document.body html.style.overflow = body.style.overflow = "hidden" const pin = () => window.scrollTo(0, TOP) pin() window.addEventListener("resize", pin) return () => { window.removeEventListener("resize", pin) html.style.overflow = body.style.overflow = "" window.scrollTo(0, 0) }}, [])Things that bit me
- Animation stutters if a
translateYmixes px withdvhorlvhinsidecalc(). Keep transforms in px. - For a sheet over the hero, clip its overhang with
clip-path: inset(0 0 BOTTOMpx 0 round …), so the sheet, not the hero, sits under the toolbar. - Test in the iOS Simulator, with a page of coloured stripes. Desktop browsers and headless Chrome can't show any of this.
- On a real phone against a Next.js dev server, add your Mac's LAN address to
allowedDevOrigins, or the phone gets a blank page.