Skip to content

fix(chat): remove phantom scrollbar in ChatLayout self-scroll mode#3575

Draft
cixzhang wants to merge 2 commits into
mainfrom
navi/fix/issue-2573-chatlayout-phantom-scroll
Draft

fix(chat): remove phantom scrollbar in ChatLayout self-scroll mode#3575
cixzhang wants to merge 2 commits into
mainfrom
navi/fix/issue-2573-chatlayout-phantom-scroll

Conversation

@cixzhang

@cixzhang cixzhang commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

In self-scroll mode (no external scrollRef), ChatLayout always overflowed its scroll container by exactly the composer/dock height, producing a phantom vertical scrollbar even when messages were far shorter than the viewport.

Root cause

packages/core/src/Chat/ChatLayout.tsx. The root is the scroll container (rootScrollableoverflowY: auto; rootflex: 1; min-height: 0) with two in-flow children:

  1. Message areamin-height: 100% (plus padding-block-end), so it alone is already the full height of the scroll container.
  2. Dockposition: sticky; bottom: 0. Sticky elements still occupy space in normal flow, so the dock adds its full height on top.

Content height = messageArea (100%) + dock height → the container overflows by the dock height regardless of message count.

Fix

  • Make the self-scroll root a flex column (rootScrollable).
  • Replace the message area's min-height: 100% with flex-grow: 1; min-height: 0 (applied only in self-scroll mode via messageAreaSelfScroll).

The message area now grows into the space left after the dock instead of forcing the container's full height, so short content no longer overflows. External-scroll mode (scrollRef provided, dock position: fixed) is unchanged.

Validation

Added tests to ChatLayout.test.tsx that assert the exact structural/style mechanism the overflow depends on, using StyleX's dev runtime injection under jsdom (getComputedStyle):

  • Before (red): message area computed min-height: 100% and the self-scroll root was display: block — the two in-flow children (100% message area + sticky dock) guaranteed overflow.
  • After (green): self-scroll root is display: flex; flex-direction: column; message area is flex-grow: 1; min-height: 0; the sticky dock and overflow: auto container are preserved.

Why this method: jsdom performs no layout, so scrollHeight/clientHeight can't be measured directly. Asserting the resolved styles pins the precise cause (a full-height in-flow child sitting beside an in-flow sticky dock) rather than a proxy. Full Chat suite: 125 passed. ESLint clean.

To eyeball (Storybook / docsite)

  • ChatLayout with few messages in self-scroll mode → no vertical scrollbar; composer docks at the bottom, empty space sits above it.
  • ChatLayout with many messages → scrolling still works and the composer stays docked.

Closes #2573

The message area was forced to the scroll container's full height
(min-height: 100%) while the in-flow sticky dock added its own height on
top, so the container always overflowed by the composer height even when
messages were short. Make the self-scroll root a flex column and let the
message area grow into the remaining space instead.

Closes #2573
@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview, Comment Jul 5, 2026 1:54am

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 4, 2026
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.6KB 0B

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

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

cixzhang commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

image

Seems to break the sticky position of the composer in the chat layout

@cixzhang

cixzhang commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Good catch — you're right, and I dug into why.

The phantom-scroll fix makes the self-scroll root a flex column and gives the message area flex-grow: 1; min-height: 0. That fixes the short-content overflow, but it directly conflicts with the dock's position: sticky; bottom: 0: in a flex-column scroll container the grown message area pushes the dock to the natural end of the content, and there's no scroll region below it for the sticky to resolve against — so with long content the composer scrolls away instead of staying pinned to the bottom.

So it's a real flex-vs-sticky tension: the flex-column layout fixes the empty-space case but breaks sticky docking, which is the opposite trade from where we started (min-height: 100% kept the dock pinned but overflowed by the dock height).

I'd rather not push a guess here since the two goals pull against each other. The direction I think is right: keep the block scroll container + sticky dock (which pins correctly on scroll) and instead stop the message area from stacking its full height on top of the in-flow dock — i.e. reserve the dock's height rather than forcing min-height: 100%. The issue's own "take the dock out of flow in self-scroll mode" option is the other candidate. Both need a call on the short-content behavior (composer glued to the bottom with empty space above, vs. sitting right under the last message).

Rather than force something visual, which do you prefer — keep sticky + reserve dock height, or move the dock out of flow? I'll implement whichever you pick and re-verify both the short-content (no scrollbar) and long-content (composer stays docked) cases.

@cixzhang

cixzhang commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

The composer inside the chat layout should be at the bottom of the layout even with short content, so the composer's position remains stable as messages stream in.

Address review: the flex-column approach fixed the phantom scrollbar but broke
the composer's sticky docking (it scrolled away with long content). Use a
single-cell grid instead: the message area and the sticky dock share one grid
cell, so the dock overlaps the message tail rather than adding flow height.
This removes the phantom scrollbar AND keeps the composer pinned to the bottom
for both short and long content, so its position stays stable as messages
stream in.
@cixzhang

cixzhang commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in 360a776. Kept the composer docked at the bottom for both short and long content by switching the self-scroll root to a single-cell grid: the message area and the sticky dock now share one grid cell, so the dock overlaps the tail of the messages instead of adding its own flow height. That removes the phantom scrollbar (which was exactly the dock height) while preserving position: sticky on the dock, so the composer stays pinned to the bottom and its position stays stable as messages stream in. Full Chat test suite passes; worth an eyeball in Storybook/docsite for the short- and long-content cases.

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 Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XDSChatLayout: self-scroll mode shows a phantom scrollbar (~composer height) when messages don't fill the viewport

2 participants