🐞 fix: Update Chrome tint implementation for iOS 26+ compatibility and enhance test coverage
Deploy to Dreamhost (dev) / build (push) Successful in 36s
Sync TODOs with Issues / sync_todos (push) Successful in 6s

This commit is contained in:
Keith Solomon
2026-07-12 14:12:18 -05:00
parent ca367364f8
commit a41f6f3834
2 changed files with 88 additions and 41 deletions
+44 -21
View File
@@ -4,24 +4,37 @@
* Tint the browser chrome (status bar + home indicator area) to match
* the site header / footer when those elements are in view.
*
* - top of screen (status bar / notch / Dynamic Island):
* controlled by the <meta name="theme-color"> tag. iOS Safari
* picks up runtime updates.
* Implementation notes — what each browser honors:
*
* - bottom of screen (home indicator area, behind the address bar):
* a fixed pseudo-element filled with `env(safe-area-inset-bottom)`
* - Chrome / older iOS Safari (≤18):
* <meta name="theme-color"> drives the top status bar tint. We
* update it dynamically. (Ignored by iOS 26+, kept for compatibility.)
*
* - iOS 26+ Safari:
* The <meta name="theme-color"> tag is ignored. WebKit instead
* samples the <body> background-color via a live observer, and
* tints the chrome to match. We set body.style.backgroundColor
* dynamically to the same color the meta tag would carry.
*
* - Bottom of the screen (home indicator area, behind the address bar):
* A fixed pseudo-element filled with `env(safe-area-inset-bottom)`
* takes its background from a body class that this module toggles
* when the footer is in view.
* when the footer is in view. (iOS 26 only samples ONE color for
* the entire chrome, so the bottom of the screen will share the
* same color as the top — we don't get a true top/bottom split on
* iOS 26. The CSS pseudo-element still serves as a no-op fallback
* for browsers that do support per-region tinting.)
*
* The header color (#032F46) is the darker end of the .site-header
* gradient. The footer color (#102C45) is the sRGB equivalent of
* the `--color-cwc-blue-02` token used by .site-footer. If those
* colors change in source, update HEADER_COLOR / FOOTER_COLOR below.
* Colors:
* - HEADER_COLOR (#032F46) is the darker end of the .site-header
* gradient.
* - FOOTER_COLOR (#102C45) is the sRGB equivalent of the
* --color-cwc-blue-02 token used by .site-footer.
*
* - "In view" = any pixel of the element overlaps the viewport.
* "In view" = any pixel of the element overlaps the viewport.
*
* No-ops if either the header or footer is missing, or if
* IntersectionObserver isn't supported (older browsers).
* No-ops if neither the header nor footer is found, or if
* IntersectionObserver isn't supported.
*/
const HEADER_COLOR = '#032F46';
@@ -39,17 +52,27 @@ function ensureThemeColorMeta() {
return meta;
}
function setTopTint(color) {
function setThemeColor(color) {
const meta = ensureThemeColorMeta();
if (color == null) {
// Clear: iOS Safari falls back to the page background.
meta.removeAttribute('content');
} else {
meta.setAttribute('content', color);
}
}
function setBottomTint(active) {
function setBodyBackground(color) {
// For iOS 26+: WebKit's live observer on <body> background-color
// drives the chrome tint. Removing the inline style lets any CSS
// background (or the default transparent) show through.
if (color == null) {
document.body.style.removeProperty('background-color');
} else {
document.body.style.backgroundColor = color;
}
}
function setBottomTintClass(active) {
document.body.classList.toggle(FOOTER_IN_VIEW_CLASS, active);
}
@@ -67,15 +90,15 @@ function initChromeTint() {
const apply = () => {
// Top tint is the header color ONLY when the header is in view.
// When only the footer is in view, the top tint is cleared (the
// user only asked for header-color at the top).
if (headerInView) {
setTopTint(HEADER_COLOR);
setThemeColor(HEADER_COLOR);
setBodyBackground(HEADER_COLOR);
} else {
setTopTint(null);
setThemeColor(null);
setBodyBackground(null);
}
// Bottom tint is the footer color ONLY when the footer is in view.
setBottomTint(footerInView);
setBottomTintClass(footerInView);
};
const buildObserver = (el, onChange) => {