facebook / facebook/astryx

[Bug] Docsite: pull-to-refresh is disabled on mobile by a global overscroll-behavior-y rule

Open
#5,392 1 comment 0 reactions 0 assignees View on GitHub
area:docs bug
Dominant language
TypeScript
Stars
13.1k
Forks
1.1k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

## Description

Pull-to-refresh does not work anywhere on the docsite on mobile. Swipe down at the top of any page — home, a component page, the blog — and nothing happens.

The cause is one line in `apps/docsite/src/app/globals.css`:

```css
/* Disable overscroll bounce so the home hero's position:fixed layers can't
show through past the top/bottom of the page. */
html {
overscroll-behavior-y: none;
}
```

`overscroll-behavior-y: none` on the root element is the standard way to turn pull-to-refresh off. The rule is unscoped, so it applies to every route.

It is the only cause. There is no `touch-action` on `html`/`body`, no `overflow: hidden` or `position: fixed` body, and the document really is the scroller (`(site)/layout`, `blog/layout` and `DocsShell` all use `AppShell height="auto"`). `playground/` and `not-found` use `height="fill"` and have an inner scroller — no pull-to-refresh there regardless, which is separate and intentional.

### Why the line exists

#3032 ([`f4784f367`](https://github.com/facebook/astryx/commit/f4784f367ec521c1d3c38f514f41d5317e9f2198)). The landing page pins the hero band, nav backdrop, aurora glow and floating cards with `position: fixed` and scrolls the showcase over them; elastic overscroll at the top/bottom lifted the in-flow content and exposed those layers behind the footer.

The side effect looks unintentional. That PR reasons about the `(site)` route group but the rule landed in app-global `globals.css`; the bug was reported on desktop ("`overscroll-behavior` reliably stops the bounce on desktop browsers (where this was reported)"); and pull-to-refresh is not mentioned in its title, summary, four-item test plan or its single review. Its own Notes section names the real alternative: "bounding the fixed hero layers to the hero region (larger change to the tuned pin-and-cover)".

### What we should do

**Remove `overscroll-behavior-y: none` entirely, at all widths, and contain the fixed hero layers instead.** Suppressing the platform's native overscroll is a global behavioural change made to work around one page's paint bug; it costs pull-to-refresh on mobile and native rubber-band feel on macOS, and it will keep costing them for anything else added to the docsite later.

This splits cleanly into two steps, and the mobile one is small.

#### Step 1 — mobile (small, verified)

Below 1024px only **one** fixed layer survives:

| layer | below 1024px |
|---|---|
| `heroContent` (`(site)/page.tsx`) | `relative` — not pinned |
| `HeroFloatingCards.stage` | `display: none` |
| `HeroThemeReel.navBackdrop` | fixed, but a 48px strip at the very top — never in the bottom overscroll gap |
| **`HeroThemeReel.backdropGlow`** | **`position: fixed`, `top: header-height`, height 1050px, `display: block` at all widths** |

On a 714px viewport the glow spans y=48–1098 for the entire page scroll. That is the whole mobile bug. With the rule overridden to `auto`, the strip under the footer goes from `#ffffff` at rest to `#fcf4e7 / #fbe4ea / #fce6f2` mid-overscroll — the three aurora blobs.

Bounding it mirrors the pattern `heroContent` already uses ~100 lines away in the same file:

```ts
// HeroThemeReel.tsx — backdropGlow
position: {default: 'absolute', '@media (min-width: 1024px)': 'fixed'},
top: {default: 0, '@media (min-width: 1024px)': 'var(--appshell-header-height, 0px)'},
```

`top` has to drop to `0` in the absolute case: `heroScope` already starts at document y=48, below the header, so keeping the header offset shifts the glow down 48px (measured — absolute put it at y=96 vs fixed's 48). `heroScope` is `position: relative` and its comment already says it exists to bound the pin-and-cover, so it is the right offsetParent. The glow then scrolls away with the hero and can never reach the footer.

#### Step 2 — desktop (the real work)

At ≥1024px three layers are genuinely pinned — `heroContent`, `stage` and `backdropGlow` — and their fixedness *is* the pin-and-cover effect, so they cannot simply be made absolute. Options worth exploring: `position: sticky` inside `heroScope` (which already wraps hero + showcase and reserves height via `heroSpacer`), or a containment/clip boundary on the hero scope. This needs a careful visual pass on the tuned effect.

macOS trackpads do rubber-band, in Safari, Chrome and Firefox alike, so this step is not cosmetic — it is the case #3032 was actually filed for. Windows and Linux have no vertical bounce, so the rule is already a no-op there.

If step 2 needs to wait, the intermediate state after step 1 is:

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

That restores pull-to-refresh on 100% of the mobile site with no visual regression, and leaves desktop exactly as it is today. It should be a stepping stone, not the destination.

### Dead end, so nobody re-walks it

Extending an opaque slab below the footer (`box-shadow: 0 100vh 0 var(--color-background-surface)` on `.footer`, which is already `position: relative; z-index: 1`) does **not** work. AppShell's `.astryx-layout-content` has `overflow: clip`, so the shadow is clipped away — confirmed applied in computed style (`rgb(255,255,255) 0px 754px 0px 0px`) with the glow still bleeding. Any "paint past the end of the document" trick inside AppShell fails the same way.

## Reproduction

1. Open https://astryx.atmeta.com/components/Button on a phone (or the iOS Simulator).
2. With the page at the top, swipe down.
3. Nothing happens — no refresh spinner, no bounce. Expected: the page reloads.

To see the bug the rule was protecting against, override it to `auto`, scroll the home page to the very bottom and rubber-band past the footer: the aurora glow shows in the gap.

Measured on a real iOS 26.5 Safari simulator (iPhone 17 Pro) by proxying the live site and injecting CSS overrides. Pull-to-refresh detected via `performance.getEntriesByType('navigation')[0].type === 'reload'`; overscroll exposure by sampling screenshot pixels in the strip under the footer.

| arm | docs page PTR | home PTR | home bottom-overscroll strip |
|---|---|---|---|
| live today | no | no | clean (bounce suppressed) |
| blanket `auto` | yes | yes | **aurora glow bleeds** |
| glow bounded + `auto` | **yes** | **yes** | **clean** |

## Astryx Version

Docsite as deployed at https://astryx.atmeta.com (`apps/docsite`, `main`)

## Environment

iOS 26.5 Safari (iPhone 17 Pro simulator). Applies to any touch browser; Chrome Android suppresses pull-to-refresh from the same property.

Contributor guide

Open the contributing guide

Research direction

Start with apps/docsite/src/app/globals.css and HeroThemeReel.tsx, then reproduce pull-to-refresh and bottom overscroll on the mobile docsite. Review heroScope, heroContent, HeroFloatingCards.stage, and HeroThemeReel.backdropGlow to preserve the desktop pin-and-cover effect while ensuring mobile refresh works and the aurora does not bleed below the footer.

Written by the indexing model from the issue text.

Assessment

Tech stack
css, typescript
Domain
frontend, web-dev
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.