safari-bar-tint
Make iOS 26+ Safari tint its status bar and floating tab bar to match the page, so a site looks native in Safari instead of sitting between white or grey bars. Covers how Safari picks the tint (root b
SKILL.mdusers/kyandesutter/claude/skills/safari-bar-tint/SKILL.md
GitHubSafari Bar Tinting (iOS 26+)
Goal: the status bar at the top and the area around Safari's floating tab bar take the page's own colours. The mechanism is undocumented by Apple; everything below is community reverse-engineering (sources at the end) plus our own failure, so the real-device gate at the bottom is not optional.
What went wrong the first time (2026-10-15)
We followed https://joshpuckett.me/seamless-mobile-safari: empty
.edge-top / .edge-bottom divs with a position: fixed coloured ::after,
max(12px, env(safe-area-inset-*)) tall. Shipped to seven sites, reverted the
same day:
- Safari ignores
::before/::afterwhen sampling. The div itself had no background, so Safari never used the strips. They were just visible bars. - The 12px minimum is visible everywhere, desktop included, and over any content that scrolls under it.
- The bottom one sat as a band above the floating tab bar and moved with it.
- Colours were picked from design tokens, not from rendered pixels.
- Verified only in the iOS Simulator, then fanned out to every site at once.
Never add decorative edge strips again. Work with what Safari actually samples.
How Safari 26 picks the tint
theme-coloris ignored by iOS 26 Safari. Keep it for Chrome on Android and older Safari, never rely on it for iOS.- Fixed or sticky elements at an edge win. Safari reads
background-color(andbackdrop-filter) of aposition: fixed/stickyelement that sits within about 4px of the top or 3px of the bottom and spans about 80% of the width. The element's own box counts; pseudo-elements and absolutely positioned children do not. - They count even when invisible.
opacity: 0andpointer-events: nonedo not exclude an element. A closed cookie banner, drawer or modal backdrop left in the layout keeps tinting the bars. - Otherwise the root background. With no qualifying fixed element, Safari
falls back to the
html/bodybackground. Transparent or unset falls back to white (light) or black (dark), which is the classic "white bar". - Images and gradients are not extended. At
scrollY = 0a photo hero gets the root background colour behind the status bar, not the photo.
The recipe
Do these in order and stop as soon as the bars look right on a device.
- Viewport.
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">. Withoutviewport-fit=covera solid bar fills the bottom safe-area gap. Addenv(safe-area-inset-*)padding to fixed headers, footers and CTAs so nothing hides under the notch or the tab bar. - Explicit, opaque root backgrounds.
htmlgets the colour of the top of the page (header or first section).bodygets the page's base colour. Both per theme (light and dark). Never transparent, and never changed at runtime: transparent sections showbodythrough them. - Audit every fixed and sticky element (
rg -n 'fixed|sticky'). For each one touching the top or bottom edge, decide if its colour should tint the bar:- Solid header you want in the status bar: leave it, it already works.
- Glass or translucent header: keep the fixed element itself transparent
and move the background and blur to an
aria-hidden, absolutely positioned child with the nav content above it. Otherwise Safari tints the bar with a half-transparent colour. - Cookie banners, drawers, modals, backdrops, toasts:
display: nonewhen closed, notopacity: 0. To animate: setdisplay: block, add the open class on the next frame; on close remove the class and setdisplay: noneafter the transition ends.
- Photo or video heroes. Set
html's background to the dominant colour of the photo's top edge as a phone crops it (sample rendered pixels from a device screenshot, not the source file and not a token). Accept that this is an approximation; do not paint anything over the photo to "fix" it. theme-colorper theme withmedia="(prefers-color-scheme: …)", matchinghtml, for other browsers.
What not to do:
- No decorative strips, bars or pseudo-elements at the edges.
- No
body { overflow: hidden }in a page shell; keephtmlscrollable. - No runtime colour swapping of
html/bodyon scroll unless a device test shows the static colour is wrong and the swap is invisible on desktop.
Rollout gate
- Apply to ONE site first.
- Check on a real iPhone running iOS 26+: top and bottom at rest, scrolled, tab bar expanded and collapsed, overscroll at both ends, light and dark, every overlay open and closed. The simulator is a first pass only.
- Check on desktop that nothing visible changed.
- The user confirms on their phone. Only then apply to the next sites, one commit per site, and ship them one at a time.
Sources
- Josh Puckett, https://joshpuckett.me/seamless-mobile-safari (the original
strip technique; its
::afterstrips did not survive a real device for us) - https://1ar.io/updates/safari-26-liquid-glass-web/ (sampling rules, thresholds, hidden-overlay and white-bar fixes)
- https://benfrain.com/ios26-safari-theme-color-tab-tinting-with-fixed-position-elements/ (theme-color dropped, fixed popovers and dialogs hijacking the tint)