Skip to content

Reserve status bar band inside nav row on iOS - #193

Open
injoon5 wants to merge 6 commits into
mainfrom
claude/safari-status-bar-transparent-mg5088
Open

Reserve status bar band inside nav row on iOS#193
injoon5 wants to merge 6 commits into
mainfrom
claude/safari-status-bar-transparent-mg5088

Conversation

@injoon5

@injoon5 injoon5 commented Aug 11, 2026

Copy link
Copy Markdown
Owner

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

  • Added --nav-safe-top CSS variable that resolves to env(safe-area-inset-top) on iOS and 0px elsewhere, capturing the status bar's reserved height
  • Updated --nav-h calculation to include the safe area inset, so scroll padding and animation timelines account for the taller header on iOS
  • Modified .nav-row padding to add --nav-safe-top to the top padding only, pushing content down while keeping the surface flush with the viewport edge
  • Adjusted --nav-name-travel calculation 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)
  • Updated documentation in CLAUDE.md and code comments explaining the Safari 26 behavior: it samples background-color and backdrop-filter from 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 transparent

Implementation 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

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
@cursor

cursor Bot commented Aug 11, 2026

Copy link
Copy Markdown

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.

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
web Error Error Aug 11, 2026 11:47am

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
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploying web with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1df0c99
Status:🚫  Build failed.

View logs

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants