Skip to content

feat(ui): scroll-edge fade affordance for scrollable surfaces - #613

Merged
crs48 merged 5 commits into
mainfrom
claude/0386-scroll-edge-fade-affordance
Jul 21, 2026
Merged

feat(ui): scroll-edge fade affordance for scrollable surfaces#613
crs48 merged 5 commits into
mainfrom
claude/0386-scroll-edge-fade-affordance

Conversation

@crs48

@crs48 crs48 commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Implements exploration 0386.

Scrollable surfaces now fade their content into the background at any edge that still has more to scroll, and the fade collapses to nothing once you reach that edge — or when the container does not scroll at all.

The core decision: mask, not overlay

The fade is a mask-image, so it dissolves the content's alpha rather than painting a background-coloured scrim on top. That is what makes it correct on every surface we have — the --surface-* ramp, .wb-root island planes, and the linear/cozy/true-black variants all differ, and a hard-coded gradient colour would be wrong on most of them. A mask carries no colour at all, so light/dark and every variant work with zero configuration.

Two mechanisms, one mask formula

  • .scroll-fade utility for raw overflow-* containers. Fade distances are @property-registered lengths animated against animation-timeline: scroll(self). When a container has no overflow its timeline is inert, so the registered 0px initial values render no fade — the "fade out when there's nothing left to scroll" behaviour falls out of the platform rather than being detected. Zero JS, no scroll listeners, no observers.
  • fade prop on ScrollArea, reading the live per-edge overflow distances Base UI publishes on its Viewport (--scroll-area-overflow-y-start/end) through min(). No timelines, no @supports gate — works in every engine including Firefox, and the scrollbar is a sibling of the masked Viewport so it stays crisp.

Also adds scroll-fade-b (bottom-only, for sticky-header containers), scroll-fade-x (RTL-aware), scroll-fade-none (escape hatch), and spacing-scale size modifiers.

Adoption

Chat transcript, channel messages, unified tree, context panel, devtools panel menu, Command/Select popup lists, settings (via the fade prop), and the board lane strip. The grid is deliberately left unfaded — mask repaints are main-thread and it has its own 60fps budget (0340).

Verification

Measured against the running app (Playwright) and cross-engine on chromium/webkit/firefox builds:

Check Result
No overflow 0px / 0px — no fade at all
Ramp continuity 0→16→32px top, 32→16→0px bottom — smooth, not a pop
WebKit (Safari engine) identical to Chromium, 0 console errors
Firefox 146 @supports excludes it → mask-image: none, 0 errors (not a stuck fade)
ScrollArea path identical in all three engines, clamps to distance left (12px left → 12px fade)
Portalled popups unaffected (mask-image: none, not descendants)
Scroll cost, 400 rows 17.6 vs 16.7 ms/frame — both at the 60fps vsync bound
Electron 33 clears the floor by version (33.4.11 → Chromium 130 ≥ 115); not booted

Verified visually over a multicolour gradient + checkerboard backdrop — a case an overlay gradient could not handle.

Notes

  • scroll-state() container queries would be the semantically perfect primitive but need Chromium 133, above Electron 33's ~130 — so the desktop app would be the one platform without it. Revisit when the floor rises.
  • Two plan deviations, recorded in the doc: Menu.tsx has no scroller to fade, and board columns don't scroll vertically (only the lane strip does). The lane strip's fade is suspended mid-drag via scroll-fade-none, because dnd-kit's DragOverlay renders inside that container.
  • A mask applies to the whole element box, so a scroller drawing its own border would see that border fade too. Every current consumer keeps its border on the parent.
  • No changeset: packages/ui is private: true, as are the app/devtools/views packages.

🤖 Generated with Claude Code

xNet Test added 5 commits July 21, 2026 06:54
Mask-based edge fade driven by scroll-driven animations for raw overflow
divs, plus a ScrollArea fade prop using Base UI's overflow variables for
cross-browser coverage.

Signed-off-by: xNet Test <test@xnet.dev>
Mask-based edge fades that dissolve content into whatever is behind it,
so they are correct on every surface ramp and theme variant without
configuration.

Two mechanisms sharing one mask formula: a .scroll-fade utility driven by
scroll-driven animations for raw overflow containers, and a fade prop on
the ScrollArea primitive that reads Base UI's live per-edge overflow
variables (works in Firefox too).

Signed-off-by: xNet Test <test@xnet.dev>
Chat transcript, channel messages, unified tree, context panel, devtools
panel menu, Command/Select popup lists, settings (ScrollArea fade prop),
and the board lane strip.

The grid is deliberately left unfaded — mask repaints are main-thread and
it has its own 60fps budget (0340). Menu has no scroller to fade, and board
columns don't scroll vertically, so both diverge from the plan; noted in
the doc.

Signed-off-by: xNet Test <test@xnet.dev>
Signed-off-by: xNet Test <test@xnet.dev>
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-613 July 21, 2026 14:42 — with GitHub Actions Inactive
@github-actions

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

Screens

✏️ Home _(SSIM 0.983)_
before after diff
before after diff
✏️ Settings _(SSIM 0.965)_
before after diff
before after diff

Interactions

🎬 Open a channel and post a message

Open a channel and post a message

▶ Watch MP4

Auto-captured by CI · run. Informational — not a blocking check.

github-actions Bot added a commit that referenced this pull request Jul 21, 2026
github-actions Bot added a commit that referenced this pull request Jul 21, 2026
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #613.

github-actions Bot added a commit that referenced this pull request Jul 21, 2026
@crs48
crs48 merged commit 9336214 into main Jul 21, 2026
21 checks passed
@crs48
crs48 deleted the claude/0386-scroll-edge-fade-affordance branch July 21, 2026 14:51
github-actions Bot added a commit that referenced this pull request Jul 21, 2026
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