Skip to content

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

Description

@imdreamrunner

Summary

Step 2 of #5392. #5415 restored pull-to-refresh on mobile by bounding the hero's aurora glow below 1024px and scoping the overscroll suppression to desktop:

@media (min-width: 1024px) {
  html { overscroll-behavior-y: none; }
}

That was always meant to be a stepping stone, not the destination. At ≥1024px three layers are still position: fixedheroContent ((site)/page.tsx), HeroFloatingCards.stage, and HeroThemeReel.backdropGlow — so the suppression has to stay, and macOS trackpads still get no native rubber-band in Safari, Chrome or Firefox. That is the case #3032 was actually filed for.

This issue is to finish the job: contain the desktop-pinned layers so overscroll-behavior-y: none can be deleted outright rather than gated.

Why sticky is the answer

A fixed layer is glued to the viewport and is not part of the document. When the document rubber-bands past its own bottom edge, the fixed layer does not lift with it — so it is sitting in the exposed gap. That is the bleed.

A sticky layer is in the document. It lifts with it, and cannot paint below the document's own bottom edge. That is structural rather than empirical, which is why it lets the rule be removed instead of narrowed.

Concretely, at max scroll today the fixed glow occupies viewport y=48–1098 — the entire viewport — so any bottom overscroll necessarily paints it.

Prototype: the effect survives

DOM surgery on a dev build, canary banner hidden to match production, one position: absolute; inset: 0 rail per layer as a direct child of heroScope, with backdropGlow + stage + heroContent all position: sticky; top: var(--appshell-header-height):

scroll 0 100 200 400 760 1400 2200 max
pixels differing from fixed 0 0 0 0 0 0 0 0

Document height unchanged (3540px). Verified at 1280×900, 1280×600, 1440×1400 and 1024×768. Layer geometry matches fixed everywhere except max scroll, where sticky releases — invisible, because the opaque showcaseOverlay already covers the hero there. Hero CTA hit-testing unaffected (elementFromPoint returns the button before and after).

So the tuned pin-and-cover effect is reproducible with sticky. The work is in the plumbing, not the design.

Two traps, both paid for already

1. The rails must go in the right place. The pinned layers are not children of heroScope. Its only two children are swipeArea (HeroReelProvider's root, 760px, holding every hero layer) and showcaseOverlay. A sticky layer inside swipeArea releases after ~48px, so each layer needs a containing block spanning hero + showcase.

Insert each rail immediately before showcaseOverlay, not at the front of heroScope. Inserting at the front puts the layers behind themeFill (opaque, still inside swipeArea, later in tree order) and produces a 29%-of-pixels difference at scroll 0 that looks exactly like sticky failing but is pure paint order.

2. display: contents on swipeArea is NOT a shortcut — I tried it. <Theme> already renders display: contents, so swipeArea is the only real box between the layers and heroScope, which makes "just make swipeArea contents and flip the three layers to sticky" look like a two-line fix. Measured, it is not:

  • document height 3540 → 5302px (+1762): heroContent becomes in-flow and adds its own height on top of heroSpacer, which still reserves 760px
  • heroContent lands at viewport y=1858 at scroll 0 — below the fold, after the spacer
  • glow x shifts 40 → −520: left: 50% + translateX(-50%) no longer resolves the way it did

Anyone taking this will reach for that shortcut. It needs the rails, plus heroSpacer bookkeeping and a centering change.

Scope

  • Three absolute; inset: 0 rails as direct children of heroScope, inserted before showcaseOverlay; the pinned layers move into them. This means the layers currently rendered by HeroReelCards inside HeroReelProvider have to be reachable as direct children of heroScope while keeping reel context and <Theme>.
  • heroContent, stage, backdropGlow: fixedsticky at ≥1024px.
  • Centering: left: 50% + transform: translateX(-50%)margin-inline: auto inside a full-width rail (verified equivalent: x=40, w=1200 either way).
  • heroSpacer height bookkeeping once the hero is sticky rather than fixed.
  • Delete @media (min-width: 1024px) { html { overscroll-behavior-y: none } } from apps/docsite/src/app/globals.css.

Add a comment where it will save someone

fixed breaks if an ancestor gains transform or filter. sticky breaks additionally if any ancestor gains overflow: hidden or overflow: auto — silently, no error, the hero just stops pinning and nobody notices until they look at the home page.

.astryx-layout-content (AppShell) already carries overflow: clip. That is safe — clip creates no scroll container — but it is one property value away from un-pinning the entire landing page. Worth a comment on that rule. (It is also why the box-shadow slab workaround in #5392 fails.)

What is not yet proven

The prototype covered static scroll positions with transitions and animations disabled, in Chromium only. Untested: the theme-swap animation mid-scroll, resize while scrolled, prefers-reduced-motion, and Safari — which is the browser the rubber-band actually matters in.

Layout equivalence is proven; the feel of the tuned effect is not. That is the careful visual pass #3032 asked for, and it is the real cost here.

Worth sizing honestly

Desktop has no pull-to-refresh. The entire payoff is native macOS trackpad rubber-band — the mobile fix restored a function, this restores a feel. Windows and Linux have no vertical bounce, so the rule is already a no-op there. It is a refactor of a hand-tuned effect for a polish win on the site's front door: worth doing deliberately, not worth rushing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions