Skip to content

fix(docsite): pin the hero with sticky rails, drop the overscroll suppression - #5480

Open
AKnassa wants to merge 5 commits into
meta:mainfrom
AKnassa:rocky/issue-5470-hero-sticky-rails
Open

AKnassa wants to merge 5 commits into
meta:mainfrom
AKnassa:rocky/issue-5470-hero-sticky-rails

Conversation

@AKnassa

@AKnassa AKnassa commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Step 2 of #5392. Fixes #5470.

What this does

Keeps the home page hero pinned exactly as it is today, but without the CSS rule that switched off the browser's native overscroll. On a Mac trackpad the page rubber-bands again at the top and bottom, like every other page on the web. Nothing changes visually.

Why

The hero's three pinned layers (the aurora glow, the floating cards and the headline block) were position: fixed. A fixed layer is glued to the screen rather than to the page, so when the page rubber-bands past its own end the layer sits in the gap that opens up. #3032 hid that by turning overscroll off for the whole site, and #5415 narrowed the rule to desktop widths to give mobile its pull-to-refresh back. This finishes the job: the layers are now position: sticky inside full-height rails, so they lift with the page and physically cannot paint past its end. That is structural, which is why the rule can be deleted instead of narrowed further.

What changed

  • Each pinned layer rides its own rail: a position: absolute; inset: 0 box that is a direct child of heroScope, spans the hero band and the showcase, and sits ahead of the showcase overlay so tree order still equals paint order (backdrop, cards, hero text, then the showcase covering them). A rail inside the 760px band alone releases its layer after about 48px, which is the trap the issue calls out.
  • backdropGlow, the cards stage and heroContent go fixed -> sticky; top: var(--appshell-header-height) at 1024px and up. Narrow widths are untouched: the glow stays absolute and the hero text stays in flow.
  • Centering moves from left: 50% + translateX(-50%) to auto margins in the full-width rail (on a sticky box left is an inset, not an offset). The 1200px box is capped to the rail's width rather than 100vw: with a classic scrollbar 100vw is wider than the rail, which zeroes the auto margins and shoves the box left.
  • HeroReelProvider renders no DOM any more. Its hover / focus / touch surface is the new HeroReelSwipeArea, which page.tsx uses as the hero text's rail, so the band and its gutters still pause the reel and the phone collage still swipes. HeroReelCards is split into HeroReelBackdrop (fills + glow) and HeroReelCards (stage).
  • The @media (min-width: 1024px) { html { overscroll-behavior-y: none } } block is deleted from globals.css.
  • Sticky also needs AppShell's main area to stay a non-scroll container (overflow: clip in LayoutContent; hidden or auto there would silently un-pin the landing page). That is recorded on the hero's rail style and enforced by the test below; core is untouched.
  • New home-hero-overscroll.test.ts. The docsite suite is node-only with StyleX untransformed, so these are source invariants in the shape of component-preview-theme.test.ts: globals.css never sets overscroll-behavior on html, :root, body or * away from auto, at any width (a contain on a nested scroller is fine; the guard has its own fixtures for @media and @supports wrappers, :is() / :where() and upper-case selectors); none of the three layers is ever fixed in any media arm; the rails are bounded by heroScope and precede the showcase; the boxes are sized against the rail, not 100vw; and the AppShell / LayoutContent links that keep the main area a non-scroll container (overflow: clip, isScrollable={isFill}, the landing layout's height="auto") hold. It is red on main today, and single-line mutations (a fixed behind a trailing comment, double quotes, a computed [BREAKPOINT] key, an overflowY longhand, isScrollable unconditional, height="fill", the width back to 100vw, heroScope static, a rail moved after the showcase, the old @media root rule, :root { overscroll-behavior: contain }, an upper-case HTML rule, a minified root rule, a root rule inside @supports) each turn it red again.

How to see it

Nothing should look different; that is the point. Run the docsite with the canary banner off so the header is the production 48px (NEXT_PUBLIC_DOCS_TARGET=latest pnpm -F @astryxdesign/docsite dev), open the home page on a Mac and rubber-band at the bottom: the page bounces and the footer stays clean. The Vercel preview on this PR shows the same page.

Measured against an untouched main checkout with Playwright + pixelmatch, both served with the banner hidden, prefers-reduced-motion: reduce so the reel holds its first slide, and transitions frozen, at ten scroll offsets per viewport (0, 100, 200, 400, 712, 760, 800, 1400, 2200, max):

viewport pixels differing from main overscroll-behavior-y fixed layers reaching the viewport bottom at page end
1280 x 900 0 none -> auto 2 -> 0
1280 x 600 0 none -> auto 3 -> 0
1440 x 1400 0 none -> auto 0 -> 0
1024 x 768 0 none -> auto 2 -> 0
900 x 1200 0 auto -> auto 0 -> 0
390 x 844 0 auto -> auto 0 -> 0

Layer geometry matches fixed at every offset (hero text at y=48 and 712px tall, glow and stage at x=40 and 1200px wide at 1280) until max scroll, where sticky releases behind the opaque showcase. Document height is unchanged (3540px). elementFromPoint on the "Get started" button still returns the button.

Also checked on both trees: hover in the gutter, on the CTA and over the pinned hero while scrolled pauses the reel, hover on the showcase does not, keyboard focus on a dot pauses and blur resumes, a phone swipe steps the reel left and right and a vertical drag does not (11 checks, identical results before and after); resize while scrolled (1280 -> 1440 -> 1100 at scroll 400) and a theme swap mid-scroll with transitions running, 0 pixels differing; and a forced 15px classic scrollbar, where the old fixed box sat at x=-7.5 overflowing both sides and the new one sits centered inside the rail.

pnpm -F @astryxdesign/docsite test (29 files, 434 tests), tsc --noEmit, strict eslint and prettier are green.

Not verified here

Safari and WebKit, and a real rubber-band gesture, which cannot be driven headlessly. That is the careful visual pass the issue budgets for.

Related

@vercel

vercel Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Error Error Sep 26, 2026 4:25am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 25, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge needs:design-review Affects visuals — Design should review labels Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current CI run completed, but its trusted analysis is unavailable.

The exact-head preview is not available yet.


Generated by PR Enrichment workflow | View current CI run

@xinzi-bot

Copy link
Copy Markdown

Thanks for finishing the desktop half of #5392. The separate rails, rail-relative 100% sizing, dedicated interaction surface, and regression coverage all look good.

Before merging, could you please make three changes?

  1. Rebase onto current main and resolve the HeroThemeReel.tsx conflict. Please preserve docs(docsite): correct two stale backdropGlow comments left by #5415 #5467’s corrected causal explanation: the glow did not itself disable pull-to-refresh; the global overscroll-behavior-y: none rule did.
  2. Drop the comment-only change to packages/core/src/Layout/LayoutContent.tsx. It makes this docsite fix appear to modify a published core component and triggers the full core review/CI path. The warning is better kept in the hero source and regression test, which already enforce overflow: clip.
  3. Narrow the globals.css regression assertion. It currently rejects every non-auto overscroll-behavior declaration anywhere in the file. That would also reject a legitimate future contain rule on a nested scroller. Please target suppression on html, :root, or another page-wide selector instead.

After the rebase, please rerun CI and verify the preview. The current Vercel and visual failures appear to be preview/infrastructure issues rather than evidence of a problem with the sticky-rail approach.

Contain the desktop-pinned hero layers so the app-global
`overscroll-behavior-y: none` can be deleted outright instead of gated
to desktop widths: macOS trackpads get their native rubber-band back.

A `fixed` layer is not part of the document, so when the document
rubber-bands past its own edge the layer sits in the exposed gap. A
`sticky` layer lifts with the document and cannot paint past its
containing block. Each pinned layer (aurora backdrop, overlap cards,
hero text) now rides its own `absolute; inset: 0` rail, a direct child
of heroScope spanning hero band + showcase and placed ahead of the
showcase overlay, as a `position: sticky` box at >=1024px. Centering
moves from `left: 50%` + `translateX(-50%)` to auto margins inside the
full-width rail, and the 1200px box is capped to the rail's width rather
than 100vw: with a classic scrollbar 100vw is wider than the rail, which
zeroes the auto margins and shoves the box left. A rail inside the 760px
band alone would release the layer after ~48px, which is why the rails
span the showcase too.

HeroReelProvider is context-only now; its hover/focus/touch surface is
the new HeroReelSwipeArea, which page.tsx uses as the hero text's rail
so the band still pauses the reel and the mobile collage still swipes.

Also records on LayoutContent why `overflow: clip` must stay `clip`:
`hidden` or `auto` there would silently un-pin the landing page. A
source-invariant test (the docsite suite is node-only) guards the rule's
absence, the three layers never going back to `fixed`, the rails' order
and containment, and the AppShell/LayoutContent links that keep the main
area a non-scroll container.

Verified against untouched main with Playwright + pixelmatch at six
viewports x ten scroll offsets: 0 px at the default threshold, identical
layer geometry, `overscroll-behavior-y: auto` everywhere, no fixed
element reaching the viewport bottom (was 2-3), 11/11 hover/focus/swipe
checks passing on both trees, resize-while-scrolled and theme-swap
mid-scroll 0 px. Safari and a real rubber-band are not verified here.

Step 2 of meta#5392. Fixes meta#5470. Supersedes the zero-height-pin approach
in meta#5431 and folds in the backdropGlow comment correction from meta#5467.
…core

Review: the comment-only change to packages/core/src/Layout/LayoutContent.tsx made a docsite fix look like a core change and pulled in the core CI path. The note on why AppShell's main area must stay overflow: clip now lives on the rail style in HeroThemeReel.tsx; home-hero-overscroll.test.ts still enforces it in LayoutContent.
Review: the guard rejected every non-auto overscroll-behavior declaration in globals.css, which would also reject a legitimate contain on a nested scroller. It now reports a declaration only when its selector subject is html, body, :root or * (case-insensitive, :is() and :where() looked through, enclosing at-rules named), and only real declarations, so an @supports (overscroll-behavior: none) prelude cannot swallow the rule inside it. The guard has its own fixtures; the page-level rule this change deletes still fails it inside any at-rule.
@AKnassa
AKnassa force-pushed the rocky/issue-5470-hero-sticky-rails branch from a6b5968 to eef4fd2 Compare August 27, 2026 03:26
@AKnassa
AKnassa marked this pull request as ready for review August 27, 2026 03:26
@AKnassa

AKnassa commented Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Done, thanks. Rebased onto current main and resolved HeroThemeReel.tsx keeping #5467's causal chain, with its tail updated for the deleted rule. Dropped the LayoutContent change; the clip note now sits on the hero's rail style and the test still enforces it in core. The globals.css guard now only reports overscroll-behavior on html, :root, body or * (looking through :is() and :where()), with its own fixtures, so a contain on a nested scroller passes.

@github-actions github-actions Bot removed the needs:code-review High-risk change (new package/component/API) — needs human code review before merge label Sep 5, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for finishing this — the sticky-rail approach is the right structural fix: bounding the glow, cards, and hero text inside full-height rails preserves the pinned hero while letting the root overscroll rule be deleted instead of narrowed again. The provider/swipe-surface split and the rail ordering also look sound in static review, and the hosted docsite tests pass.

One focused change is needed before this is ready:

  • Fix the selector classifier in home-hero-overscroll.test.ts. isPageWide currently treats any final * as page-wide, so legitimate nested rules such as body .dialog * or .dialog * with overscroll-behavior: contain would be rejected. It also only unwraps :is() / :where() when the wrapper is the whole selector compound, so composed root rules such as :is(html, body).mode or :is(html, body):hover would be missed and could bring back the site-wide overscroll suppression undetected. Please classify the actual root/body subject, handle wrappers with following class or pseudo-class suffixes, and add fixtures for both directions.

Safari/WebKit and a real trackpad rubber-band gesture remain unverified, as noted in the PR; please keep that manual check in view, but it is not the reason for this request. No change to the sticky hero runtime is being requested for this finding.

[Automated review]

…sely

Review (astracat-bot): isPageWide flagged any trailing `*` and only
unwrapped :is()/:where() when the wrapper was the whole subject, so it
both rejected legitimate nested rules and missed composed root rules.
Test-only; no change to the sticky hero runtime.

F1: a trailing `*` now counts as page-wide only on its own or as a child
or descendant of a first compound that can be the root (html, :root or
*, bare or wrapped): `*`, `*:hover`, `html *`, `:root > *`, `* > *` stay
reported; `.dialog *`, `body .dialog *`, `.sheet > *` and `.dialog * *`
with `contain` pass. Red before the change: those four were reported as
offenders.

F2: the splitter now keeps (), [] and quoted strings whole, and the
subject compound is split into simple selectors, so html, body or :root
counts anywhere in it and :is()/:where() are looked through wherever
they sit; :not()/:has() arguments are not. `:is(html, body).mode`,
`:is(html, body):hover`, `:where(html)[data-hero-dark]`,
`[data-x]:where(html)`, `html[class~='dark']` and
`body[data-mode='a b']` are now reported. Red before the change: the
guard returned [] for all six.

home-hero-overscroll.test.ts: 20/20 (16 existing, 4 new), including the
real globals.css assertion.
…oll guard

Review (astracat-bot), F1 follow-up: 16d7e27 counted a `*` subject only
on its own or under a root-capable first compound via ` ` or `>`, so it
silently allowed sibling selectors that land on body, which always follows
head: `* + *`, `* ~ *`, `head + *`, `head ~ *`, `:root > * + *`,
`html > head ~ *`. eef4fd2 reported all of them.

The `*` subject is now walked through a model of the page's top (html
holds head, then body): each compound must name the element it lands on
(its tag, `:root` for html, or `*`, bare or inside :is()/:where()), and
` `/`>` step to the parent while `+`/`~` step to the previous sibling.
So `* + *`, `head + *` and `:root > * + *` are reported; `html ~ *` and
`html + *` (html has no siblings) and nested owl selectors like
`.stack > * + *` are not. canBeRoot is folded into the same walk.

Red before the change, on the new fixtures: `expected [ …(5) ] to deeply
equal [ …(9) ]`, missing `* + *`, `* ~ *`, `head + *` and `:root > * + *`.
home-hero-overscroll.test.ts: 21/21 after, including the real globals.css
assertion. F2's fixtures are unchanged and still pass. Test-only.
@github-actions github-actions Bot added the needs:code-review High-risk change (new package/component/API) — needs human code review before merge label Sep 26, 2026
@AKnassa

AKnassa commented Sep 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, both directions reproduced on eef4fd2. I pushed two test-only commits. The sticky hero runtime is unchanged.

  • Nested * rules (16d7e27, 8b68a13): a trailing * now counts as page-wide only when it can land on html or body. That means on its own, under the root, or after head (*, html *, :root > *, * + *, head + *). With contain, .dialog *, body .dialog *, .sheet > * and .stack > * + * now pass.
  • Composed root rules (16d7e27): the splitter keeps (), [] and quoted strings whole. The subject compound counts as page-wide when any top-level part is html, body or :root, or is an :is()/:where() whose list is page-wide. :not()/:has() arguments are ignored. :is(html, body).mode, :is(html, body):hover, :where(html)[data-hero-dark], [data-x]:where(html), html[class~='dark'] and body[data-mode='a b'] are now reported.
  • New fixtures cover both directions and failed before the change. The file passes 21/21, including the assertion against the real globals.css.

Left out on purpose: CSS nesting isn't resolved, because globals.css doesn't nest. A * after a compound made only of classes or pseudo-classes (:first-child + *) isn't treated as root, and neither are namespace forms (*|*).

The Safari/WebKit trackpad rubber-band check is still manual, and I haven't done it yet.

github-actions Bot added a commit that referenced this pull request Sep 26, 2026

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This looks ready to merge: the sticky rails keep the home hero pixel-identical at rest and while scrolling, release the pinned layers before the footer at the page end, and so let the root overscroll-behavior-y: none rule go, and the new guard fails without the fix and passes with it. Before landing, please do the Safari trackpad pass you flagged at both ends of the home page, including a pull at the very top on a non-default slide: navBackdrop is still position: fixed, and a simulated 40px pull leaves its tint at the top while the nav slides over the white page canvas. The branch merges cleanly with current main and the guard passes on that merge, but CI has not run against today's AppShell, so a fresh CI run on the updated branch is worth having.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The sticky rails keep the home hero pixel-identical at rest and while scrolling, release the pinned layers before the footer, and the new guard fails without the fix and passes with it, so no code change is requested. Approval now waits on a maintainer decision: no current spec covers the docsite root's overscroll, so a maintainer needs to provide or identify the governing spec for restoring native overscroll on every docsite route at 1024px and wider (for example a claim in spec:AST-033), or explicitly accept it without one. The Safari trackpad pass you flagged is still worth doing at both ends of the home page, including a pull at the very top, where the still-fixed navBackdrop may stay behind while the nav moves.

[Automated review]

This branch had an error being deployed

1 failed deployment
Preview — 8b68a13f Deployed Sep 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docsite: contain the desktop-pinned hero layers so overscroll suppression can be removed entirely

3 participants