Skip to content

fix(BottomSheet): the grab bar's fade ends above the first line - #8514

Closed
vjeux wants to merge 3 commits into
meta:mainfrom
vjeux:uikit/c26-bottom-sheet-grab-bar-fade
Closed

vjeux wants to merge 3 commits into
meta:mainfrom
vjeux:uikit/c26-bottom-sheet-grab-bar-fade

Conversation

@vjeux

@vjeux vjeux commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

User impact

A bottom sheet at the default padding opens with its first line drawn through the grab bar's backdrop. The bar floats over the content with no reserve, so the content starts at the sheet's top edge, and the bar's surface-coloured fade reached 24 px down, past a content wrapper's default 16 px top padding: the top of the first line read washed out at rest. The review that approved #8229 lists it as a follow-up: "its first row starts 14 px down, inside the bar's 24 px fade".

The backdrop is now opaque to the pill's lower edge and transparent by the default top padding (--spacing-4). Nothing moves.

Visual authority

  • Existing component/module DD row reused or amended.
  • Durable component-local intent added to the optional Design decisions table by a team design owner.
  • Correction already settled by current family, design, theme, or objective accessibility authority.
  • New shared or cross-component direction settled in docs/design/.

None yet: the no-reserve bar is recorded only in BottomSheetPanel.tsx's comment and its test, so this asks the design owner where the fade ends. Alternatives:

  • Reserve the band with padding on the scroll body; the peek detent then also needs to count it (it overflows by 14 px otherwise), and the no-reserve test goes.
  • Leave it as it is.

Before and after

Chromium, 390×844 at 3×, light theme, the sheet's top 64 px. Showcase, at the default padding:

Showcase before Showcase after

Text Only Fitting, padding={0}:

Padding 0 before Padding 0 after

From the bar's top Before After
Backdrop opaque to 14.4 px 14 px, the pill's lower edge
Backdrop transparent by 24 px 16 px
First line, Showcase 19 px, in the fade 19 px, clear
First line, Tall Sheet 18 px, in the fade 18 px, clear

State matrix

  • Themes and modes: the colour is --color-background-surface everywhere, unchanged; only the two stops move.
  • Viewports, containers, LTR and RTL: the band is a fixed vertical 24 px.
  • Layout: unchanged. Fitting sheets still don't scroll, and the peek detent's sheet sits where it did (top 67.5 px, height 824.5 px, both).
  • Scrolled content still passes beneath the pill, through a shorter fade.

Scope

  • Appearance is the primary intent.
  • Behavior, API, interaction model, and unrelated layout changes were removed or split.
  • The evidence shows the changed pixels actively running, not only source or computed styles.
  • Public text and artifacts contain no internal Meta context.

Testing

  • BottomSheetGrabBar.a11y.chromium.spec.ts (new), against the built stories: the painted gradient's stops against the first line in Showcase and Tall Sheet, failing on main (the lines at 19 and 18 px, the fade ending at 24); the opaque part still backs the pill; four sheets sized by their content don't scroll; scrolled content still passes beneath the bar.
  • BottomSheetPanel.test.tsx: unchanged, the no-reserve test included (283 BottomSheet unit tests pass).
  • Every BottomSheet Chromium spec against the built stories (pnpm exec playwright test packages/core/src/BottomSheet/__tests__/ --config playwright.config.ts --project=chromium): all pass.
  • pnpm build, root pnpm typecheck, pnpm vitest run packages/core/src/BottomSheet packages/core/src/Popover packages/core/src/Layer packages/core/src/__tests__ (743), the CLI tests (309), pnpm check:knowledge, pnpm check:changesets, changeset coverage, CI=true pnpm lint (no error) and node scripts/check-sync.js.

The grab bar floats over the body with no reserve, so the content starts
at the sheet's top edge -- a deliberate choice its test keeps. Its
backdrop, though, stayed opaque to 60% of its 24 px band and faded out
across the rest, which reaches past a content wrapper's default 16 px top
padding: at rest the first line sat 18-19 px down, inside the fade, and
read washed out.

The backdrop is now opaque to the pill's lower edge and transparent by
the default top padding (`--spacing-4`), so the first line is clear at
rest while scrolled content still fades in beneath the pill. Layout is
unchanged: the content keeps its place, fitting sheets do not scroll and
the peek detent stays where it was.

Tests: a Chromium spec reads the painted gradient's stops against the
first line in two default-padding stories (it fails on the previous
head), checks the opaque part still backs the pill, and guards that five
fitting sheets do not scroll and that scrolled content still passes
beneath the bar; a unit test pins the stops on the style.
@vercel

vercel Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 26, 2026 10:53am UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 26, 2026, 12:05 PM UTC

…eir content

CI's accessibility run found Snap Points With Peek overflowing by 10 px.
That sheet's height is its detent's, not its content's, so whether the
content fits depends on the machine's font metrics: it fitted on the
machine the spec was written on and not on CI's. It was never a fitting
sheet, and the change under test paints only, so it cannot move it.

The guard keeps the four sheets whose height follows their content.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 26, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This visual fix is on hold for a design decision: no current spec says where BottomSheet's grab-bar fade should end, and the description asks the design owner to settle it. Approval needs that direction decided (the backdrop opaque to the pill's lower edge at 14 px and transparent by --spacing-4 at 16 px, accepting that scrolled content then passes through a 2 px fade instead of about 10 px), and an engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner. Separately, please drop the new source-text case in BottomSheetPanel.test.tsx: it restates the style declaration, which the repository's test-sufficiency rules (INV5, INV6) don't count as evidence, and the new Chromium spec already checks the rendered stops.

[Automated review]

// before the first line does, or that line reads washed out at rest; and
// the opaque part must still cover the pill. Asserted on the style
// definition for the same reason as the test above.
it('ends the bar\u2019s fade where the content\u2019s default top padding ends', async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This case asserts the style's source text, which architecture:component-test-sufficiency INV6 says does not prove the promise (and INV5 says a test must not rely on alone when the outcome can be observed more directly). It also stays green if HANDLE_BAR_HEIGHT moves to --spacing-8, which pushes the opaque stop past the 16 px fade end. Consider deleting it: BottomSheetGrabBar.a11y.chromium.spec.ts already checks the resolved stops against the first line in a real browser.

It restated the style declaration, which the test-sufficiency rules do
not count as evidence, and the Chromium spec already reads the stops the
browser paints. BottomSheetPanel.test.tsx is back to main's, the
no-reserve test included.
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Preview availability: Storybook and Sandbox are not both ready on the exact-head Vercel preview.

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/BottomSheet 26.0KB 26.0KB +10B 7.7KB 7.7KB +15B
core/BottomSheetSwitcher 9.2KB 9.2KB 0B 3.3KB 3.3KB 0B
core/ContextMenu 77.1KB 77.1KB 0B 14.5KB 14.5KB +14B
core/DateInput 92.3KB 92.3KB +13B 14.6KB 14.6KB +17B
core/DateTimeInput 102.5KB 102.5KB +16B 15.1KB 15.1KB +19B
core/DropdownMenu 72.1KB 72.1KB -2B 14.6KB 14.6KB +17B
core/Markdown 143.6KB 143.6KB +1B 17.2KB 17.2KB +11B
core/MoreMenu 72.3KB 72.3KB 0B 14.6KB 14.6KB +17B
core/MultiSelector 75.5KB 75.5KB 0B 14.5KB 14.5KB +21B
core/Pagination 87.2KB 87.2KB 0B 15.2KB 15.2KB +15B
core/Popover 49.7KB 49.7KB 0B 12.0KB 12.0KB +16B
core/PowerSearch 147.6KB 147.6KB +9B 18.5KB 18.5KB +19B
core/Selector 78.6KB 78.6KB 0B 14.8KB 14.8KB +17B
core/Table 83.3KB 83.3KB 0B 15.0KB 15.0KB +16B
core/TableCell 78.2KB 78.2KB +1B 14.6KB 14.7KB +17B
core/TableHeaderCell 78.0KB 78.0KB +1B 14.6KB 14.6KB +27B
core/TimeInput 79.2KB 79.2KB +16B 13.6KB 13.6KB +17B

Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included.

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 34.3KB 34.3KB +16B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

57 of 502 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 753,953
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,109
RichTextEditor Markdown parity: side by side probe dark 558,840
RichTextEditor Markdown parity: side by side probe light 540,756
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
AppShell With Mobile Nav probe light 217,010
AppShell With Mobile Nav probe dark 216,960
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
AppShell Playground neutral dark 65,198
AppShell Playground neutral light 63,788
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232

and 37 more.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This visual fix is on hold for a design decision: no current spec says where BottomSheet's grab-bar fade should end, and the description asks the design owner to settle it. Approval needs that direction decided (the backdrop opaque to the pill's lower edge at 14 px and transparent by --spacing-4 at 16 px, accepting that scrolled content then passes through a 2 px fade instead of about 10 px), and an engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner. The source-text unit test is gone, so nothing else is required; if an owner prefers an alternative (reserving the band in layout, or keeping the current fade), the fade needs rework.

[Automated review]

barTop: barBox.top,
barBottom: barBox.bottom,
opaqueEnd: at(stops[0] ?? '', 0),
fadeEnd: at(stops[stops.length - 1] ?? '', barBox.height),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional hardening: getComputedStyle keeps a stop's declared position even after the browser clamps it, so if the opaque stop ever passes this one (a 32 px bar puts it at 18 px), the surface is painted to 18 px while this still reads 16 px and the first-line check passes. Reading the fade end as the larger of the two stops, for example Math.max(opaqueEnd, lastStop), would catch that.

@vjeux

vjeux commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Why this matters: a bottom sheet at the default padding opens with its first line drawn through the grab bar's backdrop, so the first thing a reader sees is washed out — and this pull request cannot settle that alone, because no current spec says where the backdrop's fade ends.

The question for the design and engineering owners: should the grab bar's backdrop end its fade at 16 px (--spacing-4, where a content wrapper's default top padding ends) instead of 24 px, accepting that content scrolled beneath the pill then passes through a 2 px fade instead of about 10 px?

Our pick is yes, as built here. Measured in Chromium at 390×844:

  • The first line sits 19 px (Showcase) and 18 px (Tall Sheet) below the bar's top: inside today's fade, clear of the new one.
  • The backdrop stays opaque to the pill's lower edge (14 px), so scrolled text never runs through the pill.
  • Nothing moves: fitting sheets still don't scroll, and the peek sheet keeps its top (67.5 px) and height (824.5 px).

The before and after crops are in the description, with the two alternatives: reserve the band with padding on the scroll body (the peek detent then has to count it, since it overflows by 14 px otherwise, and the no-reserve test goes), or leave it as it is.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Thanks for picking this up. Cindy chose a different fix for the grab-bar overlap: BottomSheet keeps its content's resting top inset at 24 px or more, whatever the padding. The first row never starts under the grab bar's fade, and scrolled content still passes under the bar as it does today.

A shorter fade (this PR) leaves a sheet with smaller padding, such as Popover's sheet presentation, with its first row right at the bar's opaque edge, and scrolled content meets a near-hard edge. The inset fix clears every sheet in one place. It will be one line in BottomSheet's inset rule plus the layout change, with before-and-after captures at 390 px.

This PR will be superseded by that fix. We'll close it once the fix lands, crediting your report of the issue.

@kentonquatman kentonquatman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The new gradient is a bit too short. Visually, it looks closer to a hard cutoff than a gradient when scrolling. Would it be possible to keep the previous gradient and only have it appear once the user has started scrolling?

@vjeux

vjeux commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

A 2 px fade does read as a cut line under scrolled content, so the previous gradient stays. Cindy's fix for the overlap (above) keeps a sheet's content 24 px or more below its top at rest, which puts the whole previous fade over empty surface until something scrolls under it: it appears only once the user starts scrolling, as you describe, with no scroll listener. That fix supersedes this pull request.

The previous fade with that inset, at rest and scrolled 40 px, in Chromium and WebKit:

The previous fade with a 24 px resting inset, at rest and scrolled 40 px, in Chromium and WebKit

@vjeux

vjeux commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: an inset of at least 24 px clears every sheet in one place and keeps the previous fade, which the review above asks for as well. This pull request can close when your fix lands. If it helps, its Chromium check that reads the first line against the fade's resolved stops can come with your fix, with the fade's end at 24 px.

At padding={0}, at rest, this pull request's fade against the previous one with that inset:

Text Only at padding 0, at rest: this pull request's fade and the previous fade with a 24 px inset, in Chromium and WebKit

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Closing in favor of #8558, which landed the BottomSheet resting inset Cindy chose (at least 24 px above the content, so the grab bar never overlaps it). Thank you for the shorter-fade exploration; it helped settle the direction.

@cixzhang cixzhang closed this Oct 27, 2026
@vjeux
vjeux deleted the uikit/c26-bottom-sheet-grab-bar-fade branch October 27, 2026 13:36
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. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants