Reserve status bar band inside nav row on iOS - #193
Open
injoon5 wants to merge 6 commits into
Open
Conversation
The page is laid out `viewport-fit=cover`, so the sticky shell's `top: 0` is the display's own top edge and iOS puts the clock and the battery over the first 59px of the header — the wordmark, on every page. `--nav-safe-top` is `env(safe-area-inset-top)` and is added to the row's `padding-block-start`, not to the shell: the surface is `inset-0` of the wrapper around that row, so padding the row leaves the material spanning the band and takes only the type out of it. Padding the shell would have put the band outside the surface's box and stopped the blur short of the top of the screen, which is the thing being fixed. `--nav-h` grows by the reserve, so `scroll-padding-top`, the hero's timeline inset and the lightbox's hand-back band all clear the taller bar. `--nav-name-travel` takes it back out: the band moves the header's bottom edge and the wordmark's resting position by the same amount, and the handover is the difference — left in, the two names would sit 30px apart for the whole roll on every iPhone. It stays 44px either way. Everywhere else the inset resolves to 0px and every one of these is the number it always was; verified in Chromium at both. Safari 26 no longer reads `theme-color`. It tints its own status bar by sampling the `background-color` and `backdrop-filter` of a fixed or sticky element at the top of the viewport, skipping that element's absolutely positioned children — so the shell staying transparent is what leaves the bar transparent and lets this surface show through it. Recorded next to `.nav-surface`, because moving the tint up onto the shell now paints the header's scrolled appearance over the bar from the first frame. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F6rLcUt2Kzm7qE1qyHxKAw
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The reserve alone could not do this, and the screenshot says why: in a Safari tab the page is not laid out under the status bar at all. `env(safe-area-inset-top)` is 0 there — the header measures its shipped 56pt, the row is where it always was — so the band above it is the browser's, and no pixel of this site can reach it. Safari 26 fills that band by reading the `background-color` and the `backdrop-filter` of a fixed or sticky element flush with the top of the viewport and carrying that material up through the bar. It ignores `theme-color` outright. `.nav-shell` is the candidate it finds and it is transparent: the tint and the blur live on `.nav-surface`, which is absolutely positioned, and the scan skips a candidate's absolutely positioned children. So Safari falls back to the body's colour and paints the bar as an opaque slab, hard edged, over a header that is frosted glass with the article sliding under it. `.nav-tint` is a 4px, full-width, `opacity: 0` sliver at `top: 0` carrying the same tint and the same blur as the surface, and being read is its only job. `opacity` and not `display` or `visibility`: the scan reads styles rather than pixels, and only the properties that take an element out of layout take it out of the running. It cannot paint, cannot be hit, and is not in the accessibility tree; if a future Safari stops reading it, the bar goes back to the colour it picks today and nothing on the page moves. `color-scheme: light dark` goes with it. The site has always followed the system scheme — `app.html` reads the media query before first paint — but never said so, so everything the UA owns was still being rendered as a light page, the base under that bar included. The safe-area reserve stays. It is inert in a tab, which is what the screenshot shows, and it is the whole answer from the home screen, where the page really is laid out under the bar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F6rLcUt2Kzm7qE1qyHxKAw
Every account of what Safari 26 reads to fill its status bar is somebody's reverse engineering, and they disagree on the parts that decide this: whether an `opacity: 0` element is read, whether an absolutely positioned child of the candidate is, and whether a translucent candidate makes a translucent bar or just a paler slab. Guessing costs a trip to a phone per guess. `/safari-bar-probe.html` is six variants of one sticky bar, switched by `?v=`, each an unmistakable red: none, solid, `opacity: 0`, transparent with an absolutely positioned child (this site's structure), 50% plus a blur, and the 4px sliver as shipped. Behind them is 300vh of hard stripes, so whether the page is laid out under the bar at all is not a matter of opinion either, and a readout prints the resolved `env(safe-area-inset-top)` next to the viewport and screen heights. Not linked from anywhere and not part of the site. It comes back out before this branch merges. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F6rLcUt2Kzm7qE1qyHxKAw
Measured, on the probe, on an iPhone 16 Pro: - with no candidate at the top of the viewport the status bar is glass over the page, blurred — not a slab, and not a colour Safari invents; - an opaque `background-color` on a sticky element at the top paints the bar that colour, flat and edge to edge; - a semi-transparent one plus a `backdrop-filter` paints it as that material over the content, which is the look this header wants; - **an `opacity: 0` element is not read at all.** That last one is the sliver, and it is why nothing changed: the two write-ups that said an invisible element is still scanned are wrong. It comes out. Which also re-reads the original complaint. The bar was never a slab Safari picked — it was the article, dark and blurred, seen through glass, with the frosted header below it and the tint between them the only difference. The seam is real; the cause was not what it looked like. `env(safe-area-inset-top)` is 0px and `innerHeight` is 714 against a 874pt screen, so no pixel of this site reaches the bar and the only lever left is the scan. Handing it the tint means putting the tint on `.nav-shell`, whose box is the row alone — the disclosure grows out of an absolute panel — so it cannot be the one material for the whole header that `.nav-surface` is. That trade is a design call, not a bug fix, and is left unmade here. The probe grows a variant that answers the last open question: a candidate that is transparent at first paint and tinted three seconds later. If the bar follows, a tint that ramps with scroll can drive it and the header keeps its arrival; if it does not, only what is there at first paint counts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F6rLcUt2Kzm7qE1qyHxKAw
The material stops being a pane and becomes a gradient: the page's own colour, solid at the top edge and gone at the bottom, with the blur deepening the same way. Four masked `backdrop-filter` passes, each band starting a quarter higher than the last and overlapping the one below it by its own width — a `backdrop-filter` takes in everything painted beneath it, siblings included, so the passes compound: one radius at the bottom of the ramp and four at the top, from a step small enough that no single layer is doing the expensive thing alone. Over them, not under them, one eased scrim. A scrim that had been through the passes would be a blurred gradient, which is a gradient with its ends pulled in. It holds solid for the first eighth before it starts to give: a ramp that gives way at the very top shows the article through the pixels that are supposed to match the status bar, and on a light page that reads as dirt under the wordmark. The hairline is gone. The header has no bottom edge to draw any more. That top stop is also what lets the status bar join on. Safari 26 fills the bar with the `background-color` of a fixed or sticky element at the top of the viewport, and an opaque one paints it flat and edge to edge — measured. `.nav-shell` cannot carry it, because a colour there paints the whole row. `.nav-edge` is 4px of the ramp's own first stop, fixed at the top, over the scan's threshold and invisible by construction: the pixels directly under it are that same colour, at the top of the page and scrolled. Never faded, never animated — what counts is the value there at first paint. Two things the ramp cannot do, handled beside it: the open disclosure brings its own flat tint, because a row of links cannot sit on the transparent end of a gradient; and the deepest blur band stops at 87%, because above that the scrim is solid and a blur under an opaque colour is work nobody sees. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F6rLcUt2Kzm7qE1qyHxKAw
The gradient's box already grew with the disclosure — it is `inset: 0` of a surface that stretches — but the flat tint that kept the menu's links legible was painted over the top of it, so what opening the header actually showed was a pane, not a longer ramp. The tint moves under the scrim. A gradient composited over a flat colour is the ramp lerped into it: 1 at the top, the floor at the far end, the same shape in between. The fade now runs the whole height of the open header instead of collapsing into its top quarter, and the links still have something to sit on. Both halves of the ramp are rebuilt to be even. The blur bands were 22% wide at 19% intervals and stopped in odd places. They are now 36% wide at 18% — each starting exactly at the midpoint of the one below it — so the number of passes over any point climbs 1, 2, 3, 4 in equal measure. Bands that abut show their seams and bands that overlap unevenly read as a ramp with a lump in it. The scrim was seven stops picked by hand, which is why it was heavier above the middle than below it. It is now a smoothstep sampled at eight even intervals: symmetric, every pair either side of the middle summing to 1, and flat at both ends. The flat ends are what the hand-picked version was reaching for with a solid cap — the curve gives that for free, at the bottom as well as the top, so there is no point at which the header stops rather than ends. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F6rLcUt2Kzm7qE1qyHxKAw
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adjusts the navigation bar layout to account for the iOS status bar (clock, battery, etc.) which sits over the first 59px of the viewport when the page is laid out with
viewport-fit=cover.Summary
The header now reserves the status bar's band inside the row itself rather than letting it overlap the content. The surface (background + blur) still spans the full band, but the row's content is padded down to avoid being obscured by the status bar furniture.
Key Changes
--nav-safe-topCSS variable that resolves toenv(safe-area-inset-top)on iOS and 0px elsewhere, capturing the status bar's reserved height--nav-hcalculation to include the safe area inset, so scroll padding and animation timelines account for the taller header on iOS.nav-rowpadding to add--nav-safe-topto the top padding only, pushing content down while keeping the surface flush with the viewport edge--nav-name-travelcalculation to subtract the safe area inset, ensuring the wordmark-to-hero-name animation handover remains exactly 44px on both desktop and iPhone (the band lengthens both the header and the wordmark's rest position equally)CLAUDE.mdand code comments explaining the Safari 26 behavior: it samplesbackground-colorandbackdrop-filterfrom sticky elements at the viewport top to tint its status bar, skipping absolutely positioned children, so the tint and blur must stay on.nav-surface(not.nav-shell) to keep the status bar transparentImplementation Details
The safe area inset is a token because it affects the header's total height — anything measuring from the bottom of the bar (scroll padding, animation ranges) must see it. The alternative of padding the shell would have placed the band outside the surface's box and stopped the blur short of the screen edge, which defeats the purpose.
https://claude.ai/code/session_01F6rLcUt2Kzm7qE1qyHxKAw