Skip to content

fix(BottomSheet): the open sheet's dialog places its panel with a flex row, not a grid - #8534

Draft
vjeux wants to merge 6 commits into
meta:mainfrom
vjeux:uikit/c26-bottom-sheet-flex-host
Draft

vjeux wants to merge 6 commits into
meta:mainfrom
vjeux:uikit/c26-bottom-sheet-flex-host

Conversation

@vjeux

@vjeux vjeux commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review only this PR's change: vjeux/astryx@0cbddea...19b9372

Why

In Playwright's WebKit, an update inside an open bottom sheet lands late: the longest frame gap per "Next month" tap in a presented DateInput sheet is 74 to 87 ms, against 39 to 47 ms with this change. Every re-render in a presented sheet lays its dialog out again, and since #8229 that dialog is a grid, whose relayout WebKit takes about twice as long over.

What led to it

This comes from the work to make Astryx's touch components behave as native iOS ones do (sheets, menus, gestures and their motion). The open dialog positions its panel with no wrapper (architecture:component-style-authoring INV7); #8229 made that host a grid.

Options and tradeoffs

  • Keep the grid: no change, at about 40 ms more per update in WebKit.
  • A flex column: as cheap, but WebKit then drops a hugging sheet's content padding from its scroll range.
  • A flex row, centred and aligned to the block end: the same no-wrapper structure and the same layout, at a block's relayout cost.

Why this choice

The row keeps every reading of the grid's layout (62 of 62 across every story, detent and scroll end, in both engines) and halves WebKit's frame gap. Measured honestly: Chromium is unchanged and the iOS 26.2 simulator shows no difference (median 29 ms either way), so the benefit is WebKit's desktop engine; that question is open in the record (OQ1).

How to check it

  1. In Playwright's WebKit on a phone-sized page, open the DateInput sheet story and tap "Next month" repeatedly: the longest frame gap drops from 74 to 87 ms to 39 to 47 ms.
  2. Run BottomSheetScrollEnd.a11y.browser.spec.ts in WebKit: the hugging sheets keep their scroll-end padding with the row (a column fails it).

Evidence

  • The dialog's host style is a row flex container (justify-content: center; align-items: flex-end); a panel taller than the viewport still runs past the top edge, as with the grid.
  • component:BottomSheet's element jobs name the flex row, the one record line this changes.

Writing rubric results

R1 narration: none. R2 research: none; the measurements live here. R3 evidence: none. R4 rejected alternatives: four one-line Rejected: entries, untouched. R5 change references: none. R6 open questions: OQ1, unanswered, no options.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

…x column, not a grid

The open standalone dialog is its panel's positioner, with no wrapper
between them. It did that with a grid (`display: grid; align-content: end;
justify-items: center`). In WebKit, every re-render inside a presented
sheet relays the dialog out, and the grid host makes that relayout far
longer. Measured on a phone-sized WebKit page, four "Next month" taps in a
presented DateInput sheet, three runs: the longest gap between animation
frames was 75-221 ms (typically ~80) with the grid, against 41-53 ms with
a flex column. A content update in a Safari sheet landed visibly late.
Chromium is unaffected (28-55 ms either way).

The dialog now places the panel with a bottom-aligned, centred flex
column. The panel keeps its height there (`flex-shrink: 0`): a panel
taller than the viewport runs past the top edge, as it did in the grid,
instead of shrinking to fit.

Layout and pixels match the grid host at every detent the stories reach,
in Chromium and WebKit: every BottomSheet story at rest, Snap Points at
both stops, Snap Points With Peek at all three, and a panel forced taller
than the viewport. 33 of 34 readings are identical in layout and pixels.
In the 34th, WebKit's `[data-scroll-content]` box in Hug Height With Long
Content now matches its scroll body, as it already does in every other
WebKit sheet with long content. Its pixels are identical.
@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 27, 2026 4:18am 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 30, 2026, 1:44 PM UTC

@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.

Changes requested: the flex-column host keeps BottomSheet's layout identical in Chromium and in most WebKit states, but in WebKit a height="hug" sheet whose overflowing content is placed directly (without a padded Section) now ends with its last line flush against the bottom edge, because the content box stops growing with its content and its 16 px block-end padding falls out of the scroll range. Please keep that inset in WebKit and update the changeset's "the layout is the same"; a host where the hug panel's height stays indefinite, such as a row flex container with align-items: flex-end, avoided the loss in a layout check, though its relayout cost was not measured. Separately, component:BottomSheet still says the dialog holds the panel at the block end "with its own grid", so either keep the grid or update that line together with the host, which as a spec change needs an engineering owner's approval on the exact head.

[Automated review]

display: 'grid',
alignContent: 'end',
justifyItems: 'center',
display: 'flex',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In WebKit this host gives a height="hug" panel a definite height, so the content box's height: '100%' (BottomSheetPanel.tsx:268) resolves against the body, and WebKit ignores its minHeight: 'min-content'. With directly placed overflowing content at 390×844, the box stays at 775.5 px instead of 1745 px and its 16 px bottom padding drops out of the scroll range, so the last line ends 0.5 px from the bottom edge instead of 16.5 px. A row flex container (align-items: flex-end; justify-content: center) kept the inset in a layout check, though its relayout cost was not measured. component:BottomSheet also still says this dialog holds the panel "with its own grid".

Comment thread .changeset/bottom-sheet-flex-host.md Outdated
'@astryxdesign/core': patch
---

[fix] BottomSheet: in Safari, an update inside an open sheet no longer holds up the page about 40 ms longer than it should. The open sheet's dialog places its panel with a flex column instead of a grid; the layout is the same.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"the layout is the same" does not hold in WebKit for a height="hug" sheet whose overflowing content is placed directly: its 16 px block-end inset is lost at the scroll end.

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

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/BottomSheet 34.0KB 33.9KB -9B 7.9KB 7.9KB -22B
core/BottomSheetSwitcher 14.8KB 14.8KB 0B 3.3KB 3.3KB 0B
core/ContextMenu 77.7KB 77.7KB -1B 14.6KB 14.6KB -13B
core/DateInput 99.9KB 99.9KB -12B 14.8KB 14.8KB -13B
core/DateTimeInput 110.1KB 110.1KB -9B 15.2KB 15.2KB -14B
core/DropdownMenu 72.7KB 72.7KB +1B 14.7KB 14.7KB -12B
core/Markdown 144.5KB 144.5KB 0B 17.4KB 17.3KB -14B
core/MoreMenu 72.9KB 72.9KB 0B 14.7KB 14.7KB -12B
core/MultiSelector 76.2KB 76.2KB 0B 14.6KB 14.6KB -11B
core/Pagination 87.7KB 87.7KB +1B 15.3KB 15.3KB -17B
core/Popover 49.8KB 49.8KB 0B 12.1KB 12.1KB -9B
core/PowerSearch 155.1KB 155.0KB -10B 18.6KB 18.6KB -22B
core/Selector 79.1KB 79.1KB 0B 15.0KB 15.0KB -14B
core/Table 84.0KB 84.0KB -1B 15.1KB 15.1KB -12B
core/TableCell 78.8KB 78.8KB 0B 14.8KB 14.8KB -13B
core/TableHeaderCell 78.6KB 78.6KB 0B 14.8KB 14.8KB -13B
core/TimeInput 86.8KB 86.8KB -21B 13.8KB 13.7KB -13B

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.6KB 34.5KB -10B

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

55 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,933
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,072
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 35 more.


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

…eeps the content padding in WebKit's scroll range

The flex column host gave WebKit's content box a definite height: a hug
sheet whose overflowing content sits directly in that box lost the box's
block-end padding from the scroll range, its last line 3.5 px from the end
instead of 19.4. A flex row keeps the relayout cost of the column (39-47 ms
for the longest frame gap of a month change in a DateInput sheet, against
74-87 ms with the grid) and lays the sheet out as the grid did. The
panel's height is the row's cross size, so a sheet taller than the viewport
runs past the top edge with no flex-shrink rule.

Layout and pixels match the grid host in Chromium and WebKit, 62 of 62
readings: every story at rest, Snap Points at both stops, Snap Points With
Peek at all three, every long sheet at its scroll end, a panel forced
taller than the viewport, and two hug sheets with direct content. A WebKit
guard on those two sheets fails with the column host and passes with the
grid and the row.
@vjeux vjeux changed the title fix(BottomSheet): the open sheet's dialog places its panel with a flex column, not a grid fix(BottomSheet): the open sheet's dialog places its panel with a flex row, not a grid Oct 26, 2026
@vjeux

vjeux commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Owner question for component:BottomSheet (current): under Performance and resources, the element jobs say the dialog "holds the panel at the block end with its own grid". This change makes that a flex row (centred, aligned to the block end), with the same elements and jobs. Should the line read "with its own flex row", or name the job without the layout mode ("holds the panel at the block end")? I'll make whichever edit you prefer in this pull request, or leave the record to you.

The repository's lint requires a function that returns a promise to be
async (@typescript-eslint/promise-function-async).

@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 pull request waits on an engineering owner: component:BottomSheet still says the open dialog holds its panel "with its own grid", and the flex row also makes a percentage height such as '50%' give the sheet that share of the viewport at the bottom edge, where today a long sheet hangs from the top of the screen and a one-line sheet is cut short. An engineering owner decides how the record should describe the new host and that height behaviour, and records the decision by commenting /approve-spec <full head SHA> on this pull request; the author can do so as an engineering owner. Separately, please have the changeset name the percentage-height change instead of saying the layout is the same, and back its Safari claim with a measurement from Safari itself or limit it to WebKit as measured, since the repository's browser policy (AST-013 FR10) does not accept Playwright WebKit as Safari evidence. The checked-in BottomSheet stories and both direct-content hug sheets lay out pixel-identically on the grid and the row in Chromium and WebKit.

[Automated review]

Comment thread .changeset/bottom-sheet-flex-host.md Outdated
'@astryxdesign/core': patch
---

[fix] BottomSheet: in Safari, an update inside an open sheet no longer holds up the page about 40 ms longer than it should. The open sheet's dialog places its panel with a flex row instead of a grid; the layout is the same.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"the layout is the same" does not hold for a percentage height: with height="50%" the sheet now rises from the bottom edge at half the viewport, where the grid hung a long sheet from the top of the screen and cut a one-line sheet to 84.5 px. The Safari claim also needs a measurement from Safari itself, or wording limited to WebKit as measured (AST-013 FR10).

display: 'grid',
alignContent: 'end',
justifyItems: 'center',
display: 'flex',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

component:BottomSheet still says this dialog "holds the panel at the block end with its own grid" (Performance and resources). The row matches the grid in every checked-in story; the record's wording, and whether a percentage height should now resolve against the viewport, wait on an engineering owner.

…w; the changeset says what was measured

component:BottomSheet's element jobs said the open dialog holds the panel
at the block end with its own grid; it now names the flex row the dialog
uses. The changeset's relayout figure was measured in Playwright's WebKit
on a phone-sized page, and now says so instead of naming Safari.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
…flex row; the changeset says what was measured) into the set-height padding fix
@vjeux

vjeux commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

The line now reads '…holds the panel at the block end with its own flex row;' in 1e69c8e, the only record line this pull request changes. If it reads right, it needs an engineering owner's spec approval of 1e69c8e; to keep the grid instead, say so and the host goes back.

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 26, 2026
@vjeux

vjeux commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Measured since: the iOS 26.2 simulator shows no frame-gap difference between the grid and the flex row (29 ms median per month tap either way), so this change's only measured benefit is in Playwright's WebKit; whether to keep the grid is your call.

@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 pull request waits on an engineering owner's spec decision: it rewords component:BottomSheet's element-jobs line from "with its own grid" to "with its own flex row", and the flex row also makes a percentage height such as '50%' take that share of the viewport at the bottom edge, where today a long sheet hangs from the top of the screen and a one-line sheet is cut to 74.5 px. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request (the author can do so as an engineering owner), or asks to keep the grid. Separately, please have the changeset name the percentage-height change instead of saying the layout is the same, and either back its WebKit fix with a reading from Safari on a device or state only what was measured where, since the iOS simulator reading in this thread shows no difference (AST-013 FR10). Every named and pixel height captured, both direct-content hug sheets included, lays out pixel-identically on the grid and the row.

[Automated review]

Comment thread .changeset/bottom-sheet-flex-host.md Outdated
'@astryxdesign/core': patch
---

[fix] BottomSheet: in WebKit, an update inside an open sheet no longer holds up the page about 40 ms longer than it should (measured in Playwright's WebKit on a phone-sized page). The open sheet's dialog places its panel with a flex row instead of a grid; the layout is the same.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Two statements here don't hold on this head: "the layout is the same" is untrue for a percentage height (with height="50%" at 390×844 the grid hangs a long sheet from the top edge and cuts a one-line sheet to 74.5 px, while the row gives both a 470 px sheet at the bottom edge), and the [fix] in WebKit rests on Playwright's WebKit alone, which AST-013 FR10 says does not prove Safari behavior; the iOS 26.2 simulator reading in this thread shows no difference. Please name the percentage-height change, and either add a reading from Safari on a device or state only what was measured where.

justifyItems: 'center',
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-end',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With the row, a non-hug panel's calc(var(--_sheet-budget) + 48px) resolves a percentage against the dialog's 100dvh, so height="50%" becomes a 470 px sheet at the bottom edge, where the grid resolved it against an auto-sized row. Every named and pixel height captured lays out pixel-identically; whether the percentage result is intended, and how component:BottomSheet's element-jobs line should read, waits on an engineering owner's /approve-spec.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added 2 commits October 26, 2026 20:37
…x-row sheet host

Main's stacking applies the stack's recede to the open dialog as a
transform; this side's host lays the panel out with a flex row. Both stand:
the dialog's comment and component:BottomSheet's element-job line name the
flex row and the recede.
The iOS 26.2 simulator showed no frame-gap difference between the grid and
the flex row (29 ms median per month change either way); the gain was
measured in Playwright's WebKit on a phone-sized page only. The changeset
now says both.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 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 pull request waits on an engineering owner's spec decision: it rewords component:BottomSheet's element-jobs line from "with its own grid" to "with its own flex row", and the row also makes a percentage height such as '50%' take that share of the viewport at the bottom edge, where today a long sheet hangs from the top of the screen and a one-line sheet is cut to 74.5 px. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or asks to name the dialog's job without a layout mode or to keep the grid. If the row stays, please have the changeset name the percentage-height change instead of saying the layout is the same. Every named and pixel height, the sheet stack's recede and both direct-content hug sheets lay out pixel-identically on the grid and the row in Chromium and WebKit.

[Automated review]

'@astryxdesign/core': patch
---

[fix] BottomSheet: in Playwright's WebKit on a phone-sized page, an update inside an open sheet no longer holds up the page about 40 ms longer than it should; the iOS 26.2 simulator showed no difference. The open sheet's dialog places its panel with a flex row instead of a grid; the layout is the same.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"the layout is the same" does not hold for a percentage height: with height="50%" at 390×844 the grid hangs a long sheet from the top edge and cuts a one-line sheet to 74.5 px, while the row gives both a 470 px sheet at the bottom edge, in Chromium and WebKit. If the row stays, please name the percentage-height change here.

- the dialog carries the sheet's dialog role, modality and the top layer
when the sheet is modal, and holds the panel at the block end with its
own grid; a covered sheet's recede (FR6) is this element's transform;
own flex row; a covered sheet's recede (FR6) is this element's transform;

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 rewords a current claim ("with its own grid"), so it needs an engineering owner's /approve-spec <full head SHA> on this head; approving it also accepts the percentage-height result the row brings. Naming only the dialog's job, holding the panel at the block end, is the other wording on the table.

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:37

This branch was successfully deployed

1 active deployment
Preview — 19b93725 Deployed Oct 27, 2026 by vercel[bot]
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 needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant