Skip to content

refactor(website): make the nav bar one translucent CSS surface - #1088

Merged
blove merged 1 commit into
mainfrom
blove/nav-blur-bar
Sep 9, 2026
Merged

refactor(website): make the nav bar one translucent CSS surface#1088
blove merged 1 commit into
mainfrom
blove/nav-blur-bar

Conversation

@blove

@blove blove commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Replaces the navbar's JavaScript-driven transparent/solid switch with one translucent CSS surface, and deletes the machinery it needed.

Why

The bar switched between transparent (on HERO_ROUTES at scroll 0) and solid. That cost a useNavSurface hook, an 8px sentinel div, an IntersectionObserver, a getBoundingClientRect seed, a data-surface attribute, a HERO_ROUTES list, and 9 tests.

It also had a defect that could not be fixed, only moved. atTop initialises to false, so the server renders data-surface="solid" — the server cannot know scroll position, and effects do not run during SSR. The browser painted a white bar over the yellow hero for the duration of hydration (~250ms against next dev), then flipped and faded over 200ms. Initialising true instead merely relocates the flash to already-scrolled loads (#hash deep links, back-navigation with scroll restoration).

A single translucent blurred bar reads correctly over the yellow hero and over white content, needs no JavaScript, and has no hydration-dependent state — so it cannot flash at all.

Measured, not estimated

Sampled rendered pixels at 1440px (modal colour of a 7×7 patch via Playwright + sharp):

Bar over Sampled bar Link contrast
the yellow hero rgb(255, 232, 184) 7.86:1
/docs, /pricing, scrolled rgb(255, 255, 255) 9.44:1
the dark reliability band rgb(193, 198, 204) 5.49:1

Link contrast never drops below 5.49:1, so the links stay on --color-text-secondary — no navy switch was needed.

The blur genuinely samples live content. A hard section edge parked behind the bar renders as a smooth ~56px ramp through it (rgb(250,229,185) at y=20 → rgb(186,193,204) at y=76) against a 1px step in the source. Pure alpha would show a step, not a ramp.

0.72 alpha and 14px blur were kept: raising alpha drains the yellow character, lowering it pushes the dark-band case toward 4.5:1.

A silent failure worth knowing about

Hand-writing both backdrop-filter and -webkit-backdrop-filter breaks the blur. Lightning CSS (Next's pipeline) collapses the pair and emits only the prefixed one, and Chromium does not implement -webkit-backdrop-filter at all — CSS.supports('-webkit-backdrop-filter','blur(1px)') is false. Computed backdrop-filter was none and the bar was a flat alpha smear.

The @supports fallback cannot catch this, because Chrome does support the unprefixed property that guard asks about.

Writing only the standard property makes Lightning emit both, and expand the @supports condition itself. Recorded as a "do not hand-prefix" comment in chrome.css.

Deleted

useNavSurface.ts · useNavSurface.spec.tsx · the sentinel div · data-surface · HERO_ROUTES · .nav-scroll-sentinel · every [data-surface='transparent'] rule · the now-dead transition and its reduced-motion override.

The /docs condensation is untoucheddata-route, the flat 58px, and the demoted CTA are a separate mechanism.

Two fallbacks guard the case where a translucent bar fails to blur and becomes an unreadable smear: @supports not (backdrop-filter) and prefers-reduced-transparency: reduce, both falling back to opaque.

Tests

e2e/nav-surface.spec.ts no longer asserts a switch that does not exist. It now pins the translucent background, a non-none backdrop filter, no box-shadow, and — the guard for the whole simplification — that the bar is byte-identical before and after scrolling. A regression reintroducing state breaks it. Proven non-vacuous against a scroll-driven repaint:

✘ the nav bar surface does not change when the page is scrolled
  -   "background": "rgba(255, 255, 255, 0.72)",
  +   "background": "rgb(0, 128, 0)",

The CTA marketing-fill-vs-docs-text-link test is kept — it covers the docs condensation.

Verification

nx build exit 0 · unit 1455 passed (main's 1464 minus the 9 deleted, exactly) · nx lint 0 errors · 32 nav e2e specs pass on the rebased branch, including the strengthened hover traverse from #1087.

One judgement call left open

The Talk to Us CTA measures 1.54:1 against the bar over the hero. That is low — but the same button is 1.84:1 on plain white everywhere else on the site today, its ink is 10.73:1, and its boundary is carried by hue and its existing shadow. The bar softens an edge that was already sub-3:1 rather than creating a new failure, and retinting only here would make the nav CTA the one primary button on the site with different chrome.

Left alone deliberately. Setting border-color: var(--color-ink) on the nav CTA is a one-line change if preferred.

🤖 Generated with Claude Code

The bar switched between a transparent hero surface and a solid white one.
That switch cost a `useNavSurface` hook, an 8px sentinel div, an
IntersectionObserver, a `getBoundingClientRect` seed, a `data-surface`
attribute, a `HERO_ROUTES` list and 12 tests — all of which are deleted here.

It also had a defect that could not be fixed from inside the mechanism.
`atTop` initialised to `false`, so the server rendered `data-surface="solid"`:
the server cannot know scroll position and effects do not run during SSR, so
the browser painted a white bar over the yellow hero for the duration of
hydration (~250ms against `next dev`) before flipping to transparent. Seeding
`true` only moves the same flash to already-scrolled loads.

One CSS surface replaces it: a 72% white over `saturate(180%) blur(14px)`.
It reads over the hero's yellow and over white content alike, so there is
nothing to switch — and, because it has no hydration-dependent state at all,
there is no flash to fix.

Measured at 1440px against `next dev`, sampling rendered pixels rather than
estimating the blend:

- over the yellow hero the bar resolves to rgb(255, 232, 184); the nav links
  (`--color-text-secondary`, rgb(70, 70, 70)) sit at 7.86:1 on it, so they
  stay grey rather than moving to navy
- over white content (scrolled `/`, `/docs`, `/pricing`) the bar resolves to
  a clean rgb(255, 255, 255) behind the hairline; links 9.44:1
- over the dark reliability band it resolves to rgb(193, 198, 204); links
  5.49:1, still past 4.5:1
- the blur samples live content: a hard section edge behind the bar renders
  as a ~56px ramp through it, and the sampled bar colour changes with scroll

Two fallbacks are required because a translucent bar that fails to blur is an
unreadable smear: `@supports not (backdrop-filter: ...)` and
`prefers-reduced-transparency: reduce` both fall back to the opaque surface.
The `-webkit-` prefix is deliberately NOT hand-written — Lightning CSS emits
it, and hand-writing it makes Lightning collapse the pair to the prefixed
property alone, which Chromium does not implement at all. That silently
disables the blur, and the @supports fallback cannot catch it. See the comment
in chrome.css.

`e2e/nav-surface.spec.ts` is rewritten to assert the single surface — alpha
strictly between 0 and 1, a non-`none` backdrop-filter, no box-shadow, and an
identical computed surface before and after a scroll. That last case is the
guard for the whole simplification and was proved non-vacuous by mutation.

The `/docs` condensation (`data-route`, the flat 58px, the demoted CTA) is
untouched, and its test is kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 9, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 9, 2026 5:54am UTC

Request Review

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@blove
blove merged commit a956b25 into main Sep 9, 2026
33 checks passed
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.

1 participant