Skip to content

fix(chat): flex-column ChatLayout root so the sticky dock doesn't add phantom scroll height (#2573)#4202

Open
jiunshinn wants to merge 2 commits into
facebook:mainfrom
jiunshinn:fix/2573-chatlayout-phantom-scrollbar
Open

fix(chat): flex-column ChatLayout root so the sticky dock doesn't add phantom scroll height (#2573)#4202
jiunshinn wants to merge 2 commits into
facebook:mainfrom
jiunshinn:fix/2573-chatlayout-phantom-scrollbar

Conversation

@jiunshinn

Copy link
Copy Markdown
Contributor

Problem

In self-scrolling mode (no external scrollRef), ChatLayout always overflows its scroll container by exactly the composer/dock height, producing a phantom vertical scrollbar even when the messages are far shorter than the available height. Reported with Playwright measurements on the unmodified ai-chat docsite template in #2573.

Root cause

The root is the scroll container (overflowY: auto) with two in-flow children:

  1. messageAreaminHeight: 100% (+ paddingBlockEnd)
  2. dockContainerposition: sticky; bottom: 0

position: sticky elements still occupy space in normal flow. So total content height = messageArea (≥ 100% of the container) + dock height — the root always overflows by the dock height, regardless of message count.

Fix

Third option from the issue: make the root a flex column and let the message area flex instead of forcing its own height.

  • root: display: flex; flexDirection: column
  • messageArea: minHeight: 100%flexGrow: 1; flexShrink: 0; flexBasis: auto
  • dockContainer: flexShrink: 0 (keeps its natural height as a flex item; inert in fixed mode since position: fixed takes it out of flex layout)

With short content the message area grows to fill exactly 100% − dockHeight, so the sticky dock's natural height is part of the 100% and there is nothing to scroll. With long content flexShrink: 0 + flexBasis: auto keep the message area at its content height, the total exceeds the container, and self-scroll works exactly as before — sticky positioning is unaffected by flex layout, so the dock still pins to the scrollport bottom.

Why not the other two options

  • minHeight: calc(100% − dockHeight) — the dock's height is content-driven (the composer grows with multiline input), so the CSS var would need JS measurement (ResizeObserver) or a magic constant. The project rule is CSS-native layout; flex computes the same subtraction for free.
  • Absolute-positioning the dock in self-scroll mode — absolutely positioned children of a scroll container scroll away with the content (bottom: 0 resolves against the padding box, not the scrollport), so the dock would not stay pinned while scrolling long conversations. Making it work would mean hoisting the dock outside the scroll container — a larger DOM restructure touching root ref semantics, xstyle, and blur-layer stacking.

Measurements (real browser, before → after)

No Playwright harness exists in the repo, so I verified with a static harness: the built dist bundle (esbuild-bundled with React) + dist/astryx.css rendered in headless Chrome (agent-browser CLI, viewport 1280×720), ChatLayout inside a 600px-tall flex host, default ChatComposer. Dock height in this harness is 150px (the reporter's 186px came from the docsite template's taller composer — the invariant is diff == dockHeight).

Case Metric Before After
Short (2 msgs) clientHeight / scrollHeight 598 / 748 598 / 598
Short (2 msgs) overflow (diff) 150 (= dock height) 0
Short (2 msgs) messageArea height 598 (minHeight: 100%) 448 (= 598 − 150)
Long (60 msgs) clientHeight / scrollHeight 598 / 3806 598 / 3806 (unchanged)
Long (60 msgs) messageArea height 3656 3656 (unchanged)

Non-regression checks in the same harness, after the fix:

  • Long content, sticky dock: after scrollTop = 0, the dock stays pinned to the scrollport bottom (dockRect.bottom == rootRect.bottom).
  • First-fill auto-scroll: initial scrollTop 3208 == max scroll — useChatStreamScroll unaffected.
  • Scroll-to-bottom button: appears when scrolled up; blur backdrop present.
  • External-scrollRef mode (40 msgs, document.scrollingElement): dock is position: fixed pinned to the viewport bottom, root itself does not scroll (scrollHeight − clientHeight == 0 on root), page scrolls and auto-scrolls to bottom (1911 == max) — dockContainerFixed path unchanged.

Test plan

  • pnpm exec vitest run packages/core/src/Chat — 149/149 pass.
  • New ChatLayout — self-scroll layout contract (#2573) tests assert the class-level intent (jsdom can't do real layout): root is flex/column, message area has flexGrow: 1; flexShrink: 0 and no minHeight: 100%, dock is sticky in self-scroll mode and fixed with an external scrollRef. Verified the new tests fail (2/3) against the unfixed component; the third asserts the unchanged sticky/fixed switching.
  • tsc --noEmit -p packages/core, eslint, check:sync, check:changesets, check:package-boundaries — all pass.
  • File header updated with the new layout contract; SYNC-listed files (index exports, stories, block examples) rely only on the public API and need no changes.

Fixes #2573

Reviewer questions

  1. flexShrink: 0 on dockContainer is shared by both modes (inert under position: fixed). Would you rather it live on dockContainerSticky to keep the fixed-mode style set minimal?
  2. I used longhands (flexGrow/flexShrink/flexBasis) for explicitness next to the #2573 comment; happy to collapse to flex: '1 0 auto' if you prefer the shorthand.
  3. The related top-anchor gap issue XDSChatMessageList: add an align/anchor prop for top-aligned message lists #2572 is not addressed here, but the flex-column root probably makes a marginBlockStart: auto-style fix easier there — want me to follow up on that separately?

@vercel

vercel Bot commented Jul 22, 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 22, 2026 3:33pm

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Jul 22, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge needs:design-review Affects visuals — Design should review labels Jul 22, 2026
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

Modified Components

Chat
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 5236 -
Complexity N/A Very High (425) -

Bundle Size Summary

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

Accessibility Audit

Status: 1 accessibility violation(s) found — 1 serious.

Chat - 1 issue(s)
  • 🟠 serious: Ensure the contrast between foreground and background colors meets WCAG 2 AA minimum contrast ratio thresholds
    • Rule: color-contrast · Affects 2/8 stories · Learn more
    • WCAG: 1.4.3 (Level AA)

Generated by PR Enrichment workflow | View full report

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. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge needs:design-review Affects visuals — Design should review

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

1 participant