Skip to content

[lexical-table][lexical] Feature: Sticky scrollbar for overflowing tables#8790

Open
mayrang wants to merge 4 commits into
facebook:mainfrom
mayrang:feat/7826-table-sticky-scrollbar
Open

[lexical-table][lexical] Feature: Sticky scrollbar for overflowing tables#8790
mayrang wants to merge 4 commits into
facebook:mainfrom
mayrang:feat/7826-table-sticky-scrollbar

Conversation

@mayrang

@mayrang mayrang commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Description

When a table is wider than its container and taller than the viewport, the native horizontal scrollbar sits at the very bottom of the table — users have to scroll all the way down to reach it. This PR adds a sticky scrollbar that stays at the bottom of the viewport while the table is in view.

What changed

The createDOM path for scrollable tables now produces a three-layer structure: outerWrapper > scrollableWrapper + stickyScrollbar. The scrollableWrapper's native scrollbar is hidden via CSS, and the stickyScrollbar — a sibling div with position: sticky; bottom: 0 — provides a replacement that tracks the viewport. A spacer element inside the scrollbar matches the table's scrollWidth, and bidirectional scroll listeners keep the two in sync.

The stickyScrollbar must be a sibling of the scrollableWrapper (not inside it) because overflow-x: auto/scroll creates a scroll container that traps position: sticky.

A ResizeObserver tracks size changes to update the scrollbar width and toggle visibility. setDOMUnmanaged() prevents the reconciler from touching the scrollbar DOM.

Theme

New theme key tableStickyScrollbar for the scrollbar element. The playground theme hides the native scrollbar on the wrapper (scrollbar-width: none + ::-webkit-scrollbar { display: none }) and styles the sticky scrollbar with ::-webkit-scrollbar pseudo-elements for Chrome/Safari. Firefox uses the OS overlay scrollbar.

EditorThemeClasses.tableStickyScrollbar added to lexical.

Closes #7826

Test plan

  • pnpm vitest run --project unit -- LexicalTableNode — 38 tests pass
    • wrapTableHtml helper updated for the new outerWrapper + stickyScrollbar structure
    • 3 hardcoded HTML assertions updated (frozen column toggle, frozen column OFF, scroll changes)
  • Manual playground (Chrome / Firefox / Safari):
    • Insert 20×100 table → scrollbar sticks to viewport bottom, syncs with horizontal scroll
    • Resize window → scrollbar width updates via ResizeObserver
    • Table fits without overflow → scrollbar hidden
    • Frozen columns, row striping, alignment → all work with the new structure
  • tsc clean

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview Jul 7, 2026 5:33am
lexical-playground Ready Ready Preview Jul 7, 2026 5:33am

Request Review

mayrang added 3 commits July 7, 2026 08:48
…bles

Add a viewport-sticky horizontal scrollbar for tables that overflow their
container. The scrollbar tracks the viewport bottom via position: sticky
and syncs bidirectionally with the native scroll position.

DOM structure: outerWrapper > scrollableWrapper + stickyScrollbar (sibling,
not nested, because overflow-x traps position: sticky).

New theme key: tableStickyScrollbar on EditorThemeClasses.

Closes facebook#7826
…lbar DOM structure

Strip stickyScrollbar from actual HTML in assertHTMLOnPageOrFrame (dynamic spacer width),
update wrapTableHtml for outerWrapper > scrollableWrapper nesting,
fix WRAPPER depth and nthTableSelector for ComponentPicker and Tables specs.
…for outerWrapper

Add WRAPPER spread for inner table paths in nested table selection tests,
update regression facebook#6870 WRAPPER depth.

@etrepum etrepum left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this is a breaking change to the Table's rendered DOM, and it doesn't look like it's opt-in?

return wrapper;
}

function createStickyScrollbar(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks like this should be renamed to $createStickyScrollbar per the lint rules

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — renamed both createStickyScrollbar$createStickyScrollbar and createScrollableWrapper$createScrollableWrapper.

);

queueMicrotask(() => syncStickyScrollbar(scrollbar, tableElement));
if (typeof ResizeObserver !== 'undefined') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this a JSDOM thing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Originally added as a JSDOM guard — JSDOM doesn't provide ResizeObserver, so unit tests that call createDOM directly would throw. But since hasStickyScrollbar is opt-in now, $createStickyScrollbar only runs when explicitly enabled, so the guard is unnecessary. Removed it.

…rollbar config

Address review feedback: add hasStickyScrollbar (default false) to
TableConfig so the sticky scrollbar is fully opt-in. Rename
createScrollableWrapper/createStickyScrollbar to $ prefix per lint
rules. Remove typeof ResizeObserver guard (unnecessary with opt-in).
Only hide native scrollbar when sticky scrollbar is active.
@mayrang

mayrang commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

You're right — added hasStickyScrollbar to TableConfig (default false), so the sticky scrollbar is fully opt-in. hasHorizontalScroll alone no longer changes the DOM structure from before this PR. Playground opts in via configExtension(TableExtension, {hasStickyScrollbar: true}).

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 Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Table frozen scroll bar

2 participants