feat(ui): scroll-edge fade affordance for scrollable surfaces - #613
Merged
Conversation
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>
Contributor
🖼️ UI changes in this PRScreensInteractionsAuto-captured by CI · run. Informational — not a blocking check. |
Contributor
|
Preview removed for PR #613. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.







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-rootisland planes, and thelinear/cozy/true-blackvariants 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-fadeutility for rawoverflow-*containers. Fade distances are@property-registered lengths animated againstanimation-timeline: scroll(self). When a container has no overflow its timeline is inert, so the registered0pxinitial 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.fadeprop onScrollArea, reading the live per-edge overflow distances Base UI publishes on its Viewport (--scroll-area-overflow-y-start/end) throughmin(). No timelines, no@supportsgate — 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
fadeprop), 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:
0px / 0px— no fade at all0→16→32pxtop,32→16→0pxbottom — smooth, not a pop@supportsexcludes it →mask-image: none, 0 errors (not a stuck fade)mask-image: none, not descendants)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.Menu.tsxhas 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 viascroll-fade-none, because dnd-kit'sDragOverlayrenders inside that container.packages/uiisprivate: true, as are the app/devtools/views packages.🤖 Generated with Claude Code