Skip to content

perf(Slider): the thumbs and the fill move by translate, so moving them never lays out - #8168

Draft
vjeux wants to merge 9 commits into
meta:mainfrom
vjeux:uikit/c03-slider-transform
Draft

vjeux wants to merge 9 commits into
meta:mainfrom
vjeux:uikit/c03-slider-transform

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Rebased on #8152 (merged). #8151 and #7516 change the pointer handlers, not these styles; #8190's touch presentation builds on this.

Maintainer impact

Every value change moves the thumb and the fill through inset-inline-start/bottom and width/height, so a drag lays out the page on each move: main-thread time in every frame, and nothing a compositor animation could drive.

Evidence

iOS 26.2 simulator, Mobile Safari; each pair is main over this branch, and the gray dot is the finger.

main over this branch: LTR and RTL drag the same; in an LTR region of an RTL page, main's thumb sits 20 px past its value

Mobile Safari, previous head over this one: the tooltip stayed at the track start; now it rides its thumb

  • Layout: 51 layouts and 56 style recalculations over 60 mouse moves before, 0 and 55 after (Chromium counters, valueDisplay="none"). SliderGeometry.a11y.chromium.spec.ts now asserts it (10 layouts on main).
  • Boxes: 15 configurations (single, range, coincident, RTL, marks, disabled, text value, vertical) match main within 0.013 px in Chromium and the iOS simulator; screenshots differ only in antialiasing.

Intended invariant

  • Geometry is unchanged in LTR, RTL and vertical: thumb centers and fill ends sit where insetPosition puts them (the half-thumb inset from [Bug] Slider thumb overhangs the component bounds at min and max #5050).
  • One visible fix: in an LTR region of an RTL page, main draws the thumb 20 px past its value (its centering flips on any RTL ancestor); here the travel flips on the element's own direction.
  • component:Slider FR1–FR3: same parts and targets; the filled range stays untargeted.
  • The server renders the final position, with no post-render correction.

Change

  • The parts sit in a size container (container-type: size) filling the control's padding box, so padding moves none.
  • Each thumb publishes its progress (0–1) as a private custom property. CSS turns it into a translate against 100cqi/100cqb, flipped under :dir(rtl), as its inline start and its keys are.
  • The filled range is a full-length bar translated inside a clip shaped like the track. A mover's start edge cuts a range's start; both ends stay round, because they show through a disabled thumb.
  • A value tooltip anchors to a thumb-sized box layout places at the value.
  • Marks keep their static insets. A tooltip or text display still lays out as its text changes.

Scope

  • No intended public API, product behavior, visual direction, or policy change. (One visible fix: the mixed-direction thumb above.)
  • Product changes discovered during the work were removed or split.
  • Public text and artifacts contain no internal Meta context.

Testing

  • pnpm exec vitest run --project ui packages/core/src/Slider: 98 tests pass. The position tests check the published progress (jsdom computes neither container units nor translate), and a test covers the fill's ends in single and range mode.
  • pnpm exec playwright test packages/core/src/Slider/__tests__/SliderGeometry.a11y.chromium.spec.ts --project chromium: 8 pass. With the earlier ancestor-keyed flip, the two LTR-region-of-an-RTL-page cases fail.
  • Chromium (desktop, and iPhone 13 emulation): the box and screenshot comparison against main's build, and the layout counters.
  • Mobile Safari on the iOS 26.2 simulator (iPhone 17 Pro): the box comparison against main's build.
  • A fresh build:css of core, then pnpm exec vitest run --project node scripts/build-css.test.mjs: 11 tests pass.
  • pnpm -F @astryxdesign/core typecheck, eslint and prettier on the touched files, pnpm check:changesets, and the pre-commit checks.
  • Rebased onto main after fix(Slider): a drag belongs to the pointer that started it #8152 (the header lines and the CI step list merged by hand): the Slider suite, the geometry spec, core typecheck, the build-css test and the comparison against main were re-run on the rebased head and pass; pnpm -F @astryxdesign/storybook typecheck reports the same errors as main (unbuilt theme and chart packages, none in Slider).
  • Test-only push (8aca30a): SliderGeometry.a11y.chromium.spec.ts asserts that 59 moves of a dragged thumb (valueDisplay="none") add no Layout to Chromium's counters; 9 pass, and on main's build the new case counts 10.
  • iOS 26.2 simulator (iPhone 17 Pro), Mobile Safari, real HID touches: the GIF above, main's build against this branch's, 60 fps video cropped to the page.
  • Changeset push (f6e2b7d): the changeset now names the mixed-direction fix instead of saying rendering is unchanged; pnpm check:changesets and prettier pass.
  • Review fixes (fe7a592), from the review bot: the value tooltip anchored to the thumb's layout box, which no longer moves (the thumb travels by translate), so in Mobile Safari it sat at the track's start (measured on the iOS 26.2 simulator: tooltip centre x 30 pt with the thumb at 201; now within 1 pt of the thumb through the drag, GIF above). It anchors to a thumb-sized box that layout places at the value. And padding on the control moved the parts off the track; they now sit in a box filling the control's padding box. Geometry cases (four regions, vertical; 12 px inline padding) fail on the previous head (anchor 10–24 px off). Slider, Tooltip and a11y slider suites 160 pass; geometry spec 16; a11y Chromium slider 19; build-css 11; core typecheck; strict eslint; check:changesets, check:sync; storybook typecheck reports main's 43 errors.
  • Review fix (1cb400e), from the review bot: SliderValueTooltip sat between Slider's doc comment and export function Slider, so the emitted declaration lost its summary and @example and the Storybook docs page its introduction (checked with the build's tsc). The helper moves above the Component section (a 13-line move). SliderDocComment.test.ts checks that the last JSDoc above the component has the summary and an @example; it fails on the previous head. Slider suite 99; core typecheck; strict eslint.
  • pr-a11y on 1cb400e failed one case outside Slider: DateInput's WheelPinchZoom.a11y.chromium.spec.ts ("one finger dragged up a wheel still scrolls it": scrollTop 1652, wanted > 1656). That spec's timing flake is fixed on main by test(DateInput): the wheel pinch-zoom spec opens its minute wheel at a fixed time #8332 (merged 02:54Z, after this run's merge base). Every Slider case in the job passed. It clears at this PR's next real push.

@vercel

vercel Bot commented Oct 24, 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 28, 2026 3:37pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 24, 2026
@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 30, 2026, 12:44 AM UTC

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

Slider (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 389B 389B 0B
Lines of Code 986 1156 -
Complexity Very High Very High (160) -
Tooltip (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 412B 412B 0B
Lines of Code 577 582 -
Complexity Very High Very High (67) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
charts/Chart N/A 64.2KB N/A N/A 20B N/A
charts/ChartLegend N/A 53.5KB N/A N/A 20B N/A
charts/ChartTooltip N/A 53.5KB N/A N/A 20B N/A
core/AlertDialog 57.3KB 57.3KB +16B 12.5KB 12.5KB 0B
core/AppShell 66.8KB 66.8KB +9B 11.8KB 11.8KB 0B
core/Avatar 37.8KB 37.9KB +13B 6.3KB 6.3KB 0B
core/AvatarGroup 40.2KB 40.2KB +10B 6.4KB 6.4KB 0B
core/AvatarGroupOverflow 40.3KB 40.3KB +9B 6.4KB 6.4KB 0B
core/Banner 48.5KB 48.5KB +7B 10.1KB 10.1KB 0B
core/BaseTypeahead 55.0KB 55.0KB +12B 10.1KB 10.1KB 0B
core/BreadcrumbItem 67.3KB 67.3KB +8B 11.2KB 11.2KB 0B
core/Button 43.7KB 43.7KB +12B 9.3KB 9.3KB 0B
core/Calendar 55.6KB 55.6KB +11B 10.2KB 10.2KB 0B
core/Carousel 50.9KB 50.9KB +9B 10.3KB 10.3KB 0B
core/ChatComposer 61.3KB 61.4KB +10B 11.0KB 11.0KB 0B
core/ChatComposerInput 56.2KB 56.2KB +10B 10.2KB 10.2KB 0B
core/ChatComposerTokenElement 56.2KB 56.2KB +10B 10.2KB 10.2KB 0B
core/ChatDictationButton 47.2KB 47.2KB +8B 9.6KB 9.6KB 0B
core/ChatLayout 49.5KB 49.5KB +11B 10.1KB 10.1KB 0B
core/ChatLayoutScrollButton 47.0KB 47.1KB +11B 9.7KB 9.7KB 0B
core/ChatMessageList 29.3KB 29.3KB -1B 7.8KB 7.8KB 0B
core/ChatSendButton 46.4KB 46.4KB +12B 9.3KB 9.3KB 0B
core/ChatToolCalls 32.4KB 32.4KB 0B 9.1KB 9.1KB 0B
core/CheckboxInput 50.5KB 50.5KB +10B 9.5KB 9.5KB 0B
core/CheckboxList 46.6KB 46.6KB +10B 8.3KB 8.3KB 0B
core/CheckboxListItem 54.7KB 54.7KB +15B 10.6KB 10.6KB 0B
core/CodeBlock 61.2KB 61.2KB +9B 10.8KB 10.8KB 0B
core/CommandPalette 51.5KB 51.5KB -2B 12.0KB 12.0KB 0B
core/CommandPaletteInput 32.9KB 32.9KB +1B 8.3KB 8.3KB 0B
core/ComplexSelector 56.5KB 56.5KB +14B 10.8KB 10.8KB 0B
core/ContextMenu 75.3KB 75.3KB +11B 14.8KB 14.8KB 0B
core/DateInput 100.2KB 100.2KB +16B 14.9KB 14.9KB 0B
core/DateRangeInput 70.1KB 70.1KB +10B 11.4KB 11.4KB 0B
core/DateTimeInput 110.4KB 110.5KB +11B 15.4KB 15.4KB 0B
core/DialogHeader 49.5KB 49.5KB +12B 10.0KB 10.0KB 0B
core/DrawerHeader 49.6KB 49.6KB +13B 11.4KB 11.4KB 0B
core/DropdownMenu 73.4KB 73.4KB +23B 15.0KB 15.0KB 0B
core/DropdownMenuSubMenu 58.7KB 58.7KB 0B 9.7KB 9.7KB 0B
core/Field 44.8KB 44.8KB +11B 8.1KB 8.1KB 0B
core/FieldLabel 40.3KB 40.4KB +12B 6.7KB 6.7KB 0B
core/FileInput 54.8KB 54.8KB +10B 10.5KB 10.5KB 0B
core/Heading 9.1KB 9.1KB 0B 6.8KB 6.8KB 0B
core/IconButton 43.7KB 43.7KB +11B 9.3KB 9.3KB 0B
core/InputGroup 45.4KB 45.4KB +11B 8.2KB 8.2KB 0B
core/LayerProvider 64.0KB 64.0KB +7B 10.3KB 10.3KB 0B
core/Lightbox 51.9KB 51.9KB +12B 10.0KB 10.0KB 0B
core/Link 44.1KB 44.1KB +9B 7.7KB 7.7KB 0B
core/ListItem 54.1KB 54.1KB +15B 10.6KB 10.6KB 0B
core/Markdown 145.2KB 145.2KB +10B 17.8KB 17.8KB 0B
core/MobileNav 51.7KB 51.7KB +7B 10.2KB 10.2KB 0B
core/MobileNavToggle 46.9KB 46.9KB +10B 9.6KB 9.6KB 0B
core/MoreMenu 73.7KB 73.8KB +17B 15.0KB 15.0KB 0B
core/MultiSelector 73.6KB 73.6KB +10B 14.8KB 14.8KB 0B
core/NumberInput 56.4KB 56.4KB +11B 10.9KB 10.9KB 0B
core/Pagination 85.5KB 85.5KB +12B 15.5KB 15.5KB 0B
core/Popover 50.3KB 50.3KB +25B 12.2KB 12.2KB 0B
core/PowerSearch 166.6KB 166.6KB +3B 20.1KB 20.1KB 0B
core/ProgressBar 7.7KB 7.7KB +1B 6.6KB 6.6KB 0B
core/RadioList 46.2KB 46.2KB +11B 8.2KB 8.2KB 0B
core/RadioListItem 52.8KB 52.8KB +16B 9.8KB 9.8KB 0B
core/SegmentedControl 39.1KB 39.1KB +10B 6.3KB 6.3KB 0B
core/Selector 77.0KB 77.0KB +13B 15.2KB 15.2KB 0B
core/SideNav 64.6KB 64.6KB +7B 10.7KB 10.7KB 0B
core/SideNavCollapseButton 47.3KB 47.3KB +8B 9.6KB 9.6KB 0B
core/SideNavHeading 59.2KB 59.2KB +11B 10.4KB 10.4KB 0B
core/SideNavItem 55.3KB 55.3KB +11B 10.3KB 10.3KB 0B
core/Slider 49.1KB 49.7KB +591B 8.7KB 9.1KB +420B
core/Spinner 26.7KB 26.7KB -1B 7.5KB 7.5KB 0B
core/StatusDot 19.0KB 19.0KB +11B 6.2KB 6.2KB 0B
core/Stepper 48.1KB 48.1KB +8B 9.7KB 9.7KB 0B
core/Switch 49.0KB 49.0KB +11B 9.5KB 9.5KB 0B
core/TabMenu 55.9KB 55.9KB +10B 10.2KB 10.2KB 0B
core/Table 84.7KB 84.7KB +10B 15.6KB 15.6KB 0B
core/TableCell 76.5KB 76.5KB +12B 15.3KB 15.3KB 0B
core/TableHeaderCell 76.1KB 76.1KB +11B 15.0KB 15.0KB 0B
core/TableSelectionToolbar 52.8KB 52.8KB +11B 11.3KB 11.3KB 0B
core/Text 8.6KB 8.6KB -1B 6.8KB 6.8KB 0B
core/TextArea 50.0KB 50.0KB +10B 9.5KB 9.5KB 0B
core/TextInput 54.2KB 54.2KB +10B 10.6KB 10.6KB 0B
core/Thumbnail 51.4KB 51.4KB +13B 11.1KB 11.1KB 0B
core/TimeInput 87.5KB 87.5KB +11B 13.9KB 13.9KB 0B
core/Timer 9.2KB 9.2KB 0B 6.8KB 6.8KB 0B
core/Timestamp 27.9KB 27.9KB -1B 9.7KB 9.7KB 0B
core/Toast 60.6KB 60.6KB +10B 9.9KB 9.9KB 0B
core/ToggleButton 44.5KB 44.5KB +10B 9.4KB 9.4KB 0B
core/Tokenizer 69.3KB 69.3KB +6B 16.7KB 16.7KB 0B
core/Tooltip 16.8KB 16.8KB +12B 4.9KB 4.9KB 0B
core/TopNav 52.5KB 52.5KB +11B 10.5KB 10.5KB 0B
core/TopNavHeading 58.6KB 58.6KB +10B 10.2KB 10.2KB 0B
core/TopNavMegaMenu 55.0KB 55.0KB +11B 11.0KB 11.0KB 0B
core/TopNavMenu 58.3KB 58.3KB +10B 10.3KB 10.3KB 0B
core/Typeahead 62.2KB 62.2KB +11B 11.4KB 11.4KB 0B
lab/ChatEmojiPicker N/A 65.8KB N/A N/A 773B N/A
lab/ChatReactionBar N/A 66.3KB N/A N/A 1.1KB N/A
lab/CommentComposer N/A 94.8KB N/A N/A 1.9KB N/A
lab/CommentPin N/A 48.0KB N/A N/A 1.2KB N/A
lab/CommentThread N/A 216.8KB N/A N/A 2.4KB N/A
lab/Comments N/A 221.4KB N/A N/A 2.5KB N/A
lab/EmojiPicker N/A 63.6KB N/A N/A 722B N/A
lab/InfoTip N/A 21.9KB N/A N/A 458B N/A
lab/ListInput N/A 75.9KB N/A N/A 1.6KB N/A
lab/MobilePowerSearch N/A 147.3KB N/A N/A 1.4KB N/A
lab/MobileTokenizer N/A 92.8KB N/A N/A 809B N/A
lab/Schedule N/A 60.4KB N/A N/A 3.9KB N/A
lab/TourStep N/A 55.7KB N/A N/A 590B N/A
lab/TransferList N/A 67.5KB N/A N/A 1.7KB N/A
lab/TransferListSelector N/A 73.0KB N/A N/A 1.9KB N/A
richtext/RichTextEditor N/A 271.4KB N/A N/A 2.6KB N/A
richtext/RichTextEditorToolbar N/A 213.0KB N/A N/A 454B N/A
richtext/RichTextView N/A 251.9KB N/A N/A 2.3KB N/A

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 35.4KB 35.7KB +329B

Accessibility Audit

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

Tooltip - 1 issue(s)
  • 🟠 serious: Ensure elements that have scrollable content are accessible by keyboard in Safari
    • Rule: scrollable-region-focusable · Affects 1/21 stories · Learn more
    • WCAG: 2.1.1, 2.1.3 (Level A)

Visual Regression

55 of 504 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

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

Requesting changes: page-level LTR, RTL and vertical sliders render the same as main in Chromium, but two problems need fixing before this lands.

  • Inside a dir="ltr" region of an RTL page, the new translate flips on any [dir="rtl"] ancestor while inset-inline-start: 0 and the keyboard follow the region, so the thumbs paint outside the control and ArrowRight moves them left; keying the flip on the element's own direction with ':dir(rtl)', which StyleX lowers as-is, fixes it.
  • The #5050 inset tests were replaced by checks on the private progress properties, and with the half-thumb inset removed from the thumb's CSS all 76 Slider tests still pass, so please add a real-browser check of the thumb and fill positions at min, mid and max.
  • The changeset and file header say a value change never lays out, which holds only for valueDisplay="none"; with the default tooltip or the text display a drag still lays out on every value change.

[Automated review]

Comment thread packages/core/src/Slider/Slider.tsx Outdated
// marks. `translate` is physical, so it flips its sign under RTL.
translate: {
default: `calc(var(${THUMB_PROGRESS}) * (100cqi - ${THUMB_SIZE}px)) 0`,
':is([dir="rtl"] *)': `calc(var(${THUMB_PROGRESS}) * (${THUMB_SIZE}px - 100cqi)) 0`,

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 flip matches whenever any ancestor has dir="rtl", but the start edge (insetInlineStart: 0, line 369) and the keys (isRtlElement) follow the element's own direction. In a dir="ltr" region of an RTL page the thumb starts at the left edge and travels left: at 75 it paints 275 px outside the control, and ArrowRight moves it further left. A ':dir(rtl)' condition here and at lines 307, 331 and 342 keys the flip on the element's own direction; StyleX lowers it to a plain :dir(rtl) rule.

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.

A thumb in an LTR region of an RTL page has to travel the way its own keys and start edge do. Fixed in 986fddd (rebased from a03c859): every translate flip now keys on :dir(rtl), the element's own direction. SliderGeometry.a11y.chromium.spec.ts covers both mixed-direction regions; with the ancestor-keyed flip, its two LTR-region-of-an-RTL-page cases fail.

])(
'insets a vertical thumb at value $value so it stays in bounds',
({value, position}) => {
'places the thumb by its progress at value $value, horizontal and vertical',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These assertions read --_slider-progress, which is percent / 100 and carries no inset, so they replace the #5050 tests without covering them: with the half-thumb inset removed from the thumb's CSS travel, all 76 tests in this file still pass, while the same regression fails 6 on main. A Chromium test asserting the thumb box inside the control at min and max, and the thumb centre at the fill's end mid-range, would keep #5050 protected.

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.

The #5050 inset needs a real-browser guard, since jsdom computes neither container units nor translate. Fixed in 986fddd (rebased from a03c859): SliderGeometry.a11y.chromium.spec.ts runs in CI and checks the thumbs inside the control at min and max, and the thumb centre at the fill's end mid-range, for single and range sliders in LTR, RTL and both mixed-direction regions.

Comment thread .changeset/slider-transform-travel.md Outdated
'@astryxdesign/core': patch
---

[perf] Slider: the thumbs and the filled range move by `translate` in the control's container units instead of by inset and width, so dragging or stepping a slider no longer lays out the page on every value change. Rendering is unchanged.

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 holds only for valueDisplay="none": in Chromium a 60-move drag of the default slider (value tooltip) lays out 30 times both before and after this change, and the text display 15 times both ways. Suggest scoping it to the parts that moved, for example "…so the thumbs and the filled range no longer lay out on every value change; a displayed value still does." The @output header (Slider.tsx line 8) and the THUMB_PROGRESS comment (line 169) say the same.

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.

The claim should match what a drag actually costs. Fixed in 986fddd (rebased from a03c859): the changeset, the @output header and the THUMB_PROGRESS comment now say that moving the thumbs and the fill never lays out, while a value tooltip or text display still does as its text changes. The PR title and description say the same now.

vjeux and others added 2 commits October 24, 2026 07:36
…hange never lays out

The thumb and the filled range were placed with inset-inline-start/bottom and
width/height, so every value change during a drag laid out the page (51 layouts
over 60 mouse moves). They now travel by translate, computed in CSS from a
progress custom property against the control's container units
(container-type: size): the server still renders the right place, RTL and
vertical keep their geometry, and a value change only restyles. The filled range
is a full-length bar translated inside the track's clip, so it moves the same
way.
…wser geometry contract; narrow the layout claim
@vjeux
vjeux force-pushed the uikit/c03-slider-transform branch from a03c859 to 986fddd Compare October 24, 2026 14:42
@vjeux vjeux changed the title perf(Slider): the thumbs and the fill move by translate, so a value change never lays out perf(Slider): the thumbs and the fill move by translate, so moving them never lays out Oct 24, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 24, 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.

Requesting changes: the translate-based travel renders like main in Chromium (LTR, RTL, range, marks and vertical), keeps the value tooltip on the thumb in Chromium and WebKit, fixes the thumb inside an LTR region of an RTL page, and saves the layouts the changeset now describes for valueDisplay="none". One gap remains: the vertical #5050 position test now reads only --_slider-progress and the new geometry spec mounts only horizontal stories, so removing the half-thumb inset from thumbVertical leaves every Slider test passing. Please add a vertical case (for example core-slider--vertical-orientation) to SliderGeometry.a11y.chromium.spec.ts that checks the thumb inside the control at Home and End and the fill ending at the thumb's centre.

  • Not blocking: on a control padded through controlStyle or controlXstyle, 100cqi measures the content box while the thumb starts at the padding edge, so the thumb lands twice the padding short of the last mark and "Rendering is unchanged" in the changeset does not hold there.

[Automated review]

/>,
);
expect(screen.getByRole('slider')).toHaveStyle({bottom: position});
expect(progressOf(screen.getByRole('slider'))).toBe(progress);

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 vertical check reads --_slider-progress, which carries no inset, and SliderGeometry.a11y.chromium.spec.ts mounts only horizontal stories, so nothing replaces the #5050 vertical inset test it took over: with (0px - 100cqb) in thumbVertical (Slider.tsx:383) all 98 Slider tests still pass. A vertical case in the geometry spec (thumb inside the control at Home and End, fill ending at the thumb's centre) would restore it.

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.

Covered by 98ada67 (Cindy Zhang's commit): SliderGeometry.a11y.chromium.spec.ts mounts the vertical story and checks that the thumb stays inside the control and the fill follows the value. With (0px - 100cqb) in thumbVertical it fails (the thumb is 20 px out, against a limit of 1 px).

…ayed

A real-browser trace of the claim the layout counters showed by hand: 59 mouse
moves of a dragged thumb (valueDisplay none) add no Layout to Chromium's
performance counters. The same test on main counts 10.

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

Requesting changes: the translate-based travel renders like main in Chromium (LTR, RTL, range, marks, vertical and the value tooltip), fixes the thumb inside an LTR region of an RTL page, and the drag the new test performs lays out 0 times with valueDisplay="none" where main lays out 10. One gap from the earlier review remains: the vertical #5050 test now reads only --_slider-progress and SliderGeometry.a11y.chromium.spec.ts mounts only horizontal stories, so removing the half-thumb inset from thumbVertical still leaves every Slider test passing. Please add a vertical case (for example core-slider--vertical-orientation) that checks the thumb inside the control's top and bottom edges at Home and End and the fill ending at the thumb's centre.

  • Not blocking: on a control padded through controlStyle, controlXstyle or a theme rule, 100cqi measures the content box while the thumb starts at the padding edge, so the thumb lands twice the padding short of the last mark and of the fill's end.

[Automated review]

test(`a single slider's thumb and fill follow its value: ${region.name}`, async ({
page,
}) => {
await mount(page, 'core-slider--default', region.page, region.region);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Every case in this spec mounts a horizontal story (here, line 162 and line 190) and measure() reads only left and right edges, so nothing replaces the vertical #5050 test: with (0px - 100cqb) in thumbVertical (Slider.tsx line 383) all 98 Slider tests still pass. A core-slider--vertical-orientation case that checks the thumb inside the control's top and bottom edges at Home and End, and the fill ending at the thumb's centre, would restore it.

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.

Same as the thread above: 98ada67 added a vertical case to this spec ("a vertical slider's thumb stays inside the control and its fill follows the value"), and fe7a592 adds a vertical tooltip-anchor case. With (0px - 100cqb) in thumbVertical the first one fails, 20 px out.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 24, 2026
…fari)

Recordings of the iOS 26.2 simulator (iPhone 17 Pro) in Mobile Safari, cropped to
the page; the gray dot is the finger, drawn by the test page. main against each
pull request: meta#8151 coincident range thumbs, meta#8168 translate travel, meta#8190 the
touch presentation and its held-thumb fix.
On main a Slider in a left-to-right region of a right-to-left page draws its
thumb one thumb width past its value (its centering transform flips on any
RTL ancestor); the travel here flips on the element's own direction, so the
changeset no longer says rendering is unchanged without that exception.

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

Requesting changes for one small fix. The translate-based travel keeps Slider's geometry in Chromium and Firefox, puts the thumb on its value in an LTR region of an RTL page, centres the value tooltip on its thumb in Firefox, and takes a valueDisplay="none" drag from 10 layouts to 0. SliderValueTooltip now sits between Slider's doc comment and export function Slider (Slider.tsx:574-628), so the comment no longer attaches to Slider: its emitted declaration loses the description and example, and the Storybook docs page loses its introduction; moving SliderValueTooltip above that comment, or below Slider, fixes it.

[Automated review]

* <Slider label="Price range" value={[20, 80]} onChange={setRange} />
* ```
*/
/**

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Slider's doc comment (lines 574-582) now leads this function instead of export function Slider (line 628), so it attaches to nothing: the declaration emitted for Slider loses its summary and @example, and Storybook's docgen description for Slider is empty. Moving SliderValueTooltip with its comment above line 574, or below Slider, puts the comment back on the component.

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.

Fixed in 1cb400e: SliderValueTooltip now sits above the Component section, so the doc comment is directly above export function Slider again. The build's tsc now emits the summary and the @example above export declare function Slider; on the previous head there was no comment there. A new case, SliderDocComment.test.ts, parses Slider.tsx and checks that the last JSDoc block above the component carries the summary and an @example. It fails on the previous head.

SliderValueTooltip was declared between Slider's doc comment and
`export function Slider`, so the comment attached to the helper: the
declaration emitted for Slider lost its summary and example (checked with the
build's tsc), and the Storybook docs page lost its introduction. The helper now
sits above the Component section. A case parses Slider.tsx and checks that the
last JSDoc block above `export function Slider` carries the summary and an
`@example`; it fails on the previous head.

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

Recommending approval: the translate-based travel keeps Slider's geometry in Chromium and Firefox, puts the thumb on its value in an LTR region of an RTL page, centres the value tooltip on its thumb in Firefox, and takes a valueDisplay="none" drag from 10 layouts to 0. Slider's doc comment sits on export function Slider again, and SliderDocComment.test.ts now guards it. The red pr-a11y job fails only a DateInput wheel-scroll case this change does not touch, while its Slider geometry step passes, so a rerun of that job is the only follow-up.

[Automated review]

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
…x and moves by the thumb's translate

Engines disagree on whether CSS anchor positioning follows a `translate`
on the anchor: Chromium anchors to the moved box, Firefox to the layout
box. Instead of a thumb-sized anchor that layout places at the value (an
extra element per thumb, laid out on every value change), the value
tooltip now anchors to the travel box, which no transform moves, so every
engine resolves the same anchor, and it moves by the same `translate` as
its thumb, from the same progress, so there is no double translation:

- Horizontal: the tooltip starts at the travel's start edge and travels
  by the thumb's progress in container units, mirrored under :dir(rtl).
- Vertical: it starts at the travel's bottom and travels up; the travel
  box is as wide as the field, so a negative inline margin pulls it in
  beside the centred thumb, which the inline flip mirrors with the side.
- Both clamp to the travel box, so a label wider than the thumb shifts
  inward at the ends instead of leaving the control, and only the
  placement-axis flip is kept (an alignment fallback would move the edge
  the translate starts from).

useTooltip's renderTooltip now applies the `xstyle` and `style` its
second argument already accepted, which carries the translate.

A new browser spec runs in Chromium, Firefox and WebKit in CI: the
tooltip is centred on the thumb at 10/25/50/75/90, over the thumb and
inside the control at both ends, flipped below with no room above, RTL,
vertical, both thumbs of a range, forced colors and reduced motion, with
screenshots and a drag clip per engine uploaded as evidence. The
geometry spec's anchor-box checks move there.
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

The new SliderValueTooltip.a11y.browser.spec.ts in 5863937 doesn't load. Playwright 1.61 rejects test.use({video}) inside describe('drag clip') ("Cannot use({ video }) in a describe group, because it forces a new worker"), so --list finds 0 tests.

Commit 9f0aadb (branch slider-tooltip-spec-video-scope on the vjeux fork, a fast-forward on 5863937) moves that option to file scope. With it, on a full Storybook build in Chromium, 2 of 7 pass and 5 fail, each on "above the thumb":

  • "no double translation (ltr)" and "(rtl)": 50: above the thumb, tooltip bottom 92 against the thumb's top 40 (tolerance 1.5);
  • "both thumbs of a range": thumb 0: above the thumb;
  • "forced colors and reduced motion": ArrowRight: above the thumb;
  • "drag clip": drag at 0.5: above the thumb.

This head's typecheck red is the lab MobileTokenizer.test.tsx TS2348 failure, fixed on main by #8313. A merge of main clears it.

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

Requesting changes: the translate-based thumb and fill travel keeps Slider's geometry in Chromium and Firefox and puts the thumb on its value in an LTR region of an RTL page, but the newest commit's value-tooltip placement regresses. Please place the value tooltip with geometry that does not depend on where its layer is hosted (the previous revision's layout-placed anchor box under the layer's own anchor-mode placement worked), and add tooltip cases for a Slider inside a list or table cell and for mixed-direction regions.

  • Inside a ul > li or a table cell, the tooltip renders 380–388 px from its thumb in Chromium and Firefox, because 100cqi stops measuring the track once the layer is portaled out of the slider.
  • In Firefox, an RTL region on an LTR page (or the reverse) puts the tooltip off-screen, for example at x −592 for a thumb at 640.
  • Passing positionTryFallbacks through renderTooltip's style conflicts with spec:AST-059 FR4, and keeping the travel box element in React state conflicts with architecture:react-component-runtime INV1.

[Automated review]

// keeps it in the viewport wherever the slider is.
valueTooltipHorizontal: {
translate: {
default: `clamp(0px, ${THUMB_INSET_PX}px + var(${TOOLTIP_PROGRESS}) * (100cqi - ${THUMB_SIZE}px) - 50%, 100cqi - 100%) 0`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

100cqi here measures the travel box only while the tooltip is hosted inside it. When the Slider sits in a ul, table, label, span, link or button, useLayer portals the tooltip outside that ancestor and the units resolve against another container or the viewport: in a table cell at 50 the tooltip lands at 784 px against a thumb at 396 px, in Chromium and Firefox. Anchoring to a box that layout places at the value, as the previous revision did, keeps the placement independent of hosting.

// Only the placement-axis flip: the translate already keeps the tooltip
// inside the control on the other axis, and an alignment fallback would
// move the edge the translate starts from.
positionTryFallbacks: isHorizontal ? 'flip-block' : 'flip-inline',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-059 FR4: “The runtime authors this order; a component passes no fallbacks of its own to an anchor-mode layer.” renderTooltip keeps anchor mode, so this override, together with the self-authored translate geometry, takes over placement the runtime owns (architecture:layer-runtime INV3).

const trackRef = useRef<HTMLDivElement>(null);
const draggingThumbRef = useRef<number | null>(null);
// The travel box: every part sits in it, and the value tooltips anchor to it.
const [travelBox, setTravelBox] = useState<HTMLDivElement | null>(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

architecture:react-component-runtime INV1: “DOM nodes and whole DOM collections MUST NOT be stored in React state.” This also gives every Slider a second render on mount and needs the layout effect in SliderValueTooltip, which names no external system (INV3). A callback ref that attaches the tooltip anchor, as the previous revision's ref={tooltip.positionRef} did, avoids both.

);
await page.locator('.astryx-slider-control').first().waitFor();
await page.evaluate(dir => {
document.documentElement.dir = dir;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Setting the page direction to the slider's own means no mixed-direction case runs. In Firefox an RTL region of an LTR page puts the tooltip at x −592 for a thumb at 640, and an LTR region of an RTL page at 2432 in a 1280 px viewport; the geometry spec's four REGIONS would catch it here.

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

The review's suggested fix would bring back the layout-placed anchor box that 5863937 removed on purpose, so there are two ways forward:

  • (a) Keep this design and measure the tooltip's placement from its anchor instead of container units, so a portaled layer and a mixed-direction region resolve the same way.
  • (b) Restore the layout-placed anchor box from 1cb400e, under the layer's own anchor-mode placement.

Either one will be implemented once chosen, with tooltip cases for a list item, a table cell and mixed-direction regions.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
# Conflicts:
#	.github/workflows/ci.yml
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Re-asking once on the tooltip placement, since it's the open choice here:

  • (a) keep 5863937's design and place the tooltip from its anchor instead of container units;
  • (b) restore the layout-placed anchor box from 1cb400e.

My pick is (b): it places the tooltip correctly in lists, table cells, labels and mixed-direction regions. #8190 carries (b) today, so if you pick (a), #8190 re-merges this PR's final head before it merges.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
# Conflicts:
#	.github/workflows/ci.yml
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
# Conflicts:
#	.github/workflows/ci.yml
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
# Conflicts:
#	.github/workflows/ci.yml
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
@vjeux

vjeux commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

An offer for this PR's conflict with main, as two commits on your head: https://github.com/vjeux/astryx/commits/offer/8168-main-fold

It folds main and takes its workflow, whose discovery routing forbids the Slider steps this branch added, and it makes the value tooltip spec run: Playwright refused the file over a describe-level video option, and two setups needed fixing (room above the control, and ArrowUp instead of ArrowRight under RTL). It then passes in Chromium, Firefox and WebKit. Take it or leave it.

The workflow is main's: since meta#8367 pr-a11y discovers browser specs and
ci-test-routing.test.mjs forbids naming one in a step, so the Slider steps
this branch added go; its Slider specs run through discovery.

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

Approval waits on an engineering owner's decision about one addition. useTooltip's renderTooltip now applies a caller's xstyle and style, and no current spec governs that hook. An engineering owner (the author may decide, as one) needs to approve it and name its governing record, or rule it out, by commenting /approve-spec <full head SHA> on this pull request. Whatever that decision, these need fixing:

  • In a list, table, label, link, button or span, the value tooltip is portaled out of the travel box, so its 100cqi stops measuring the track and a 500 px Slider shows it 406 px from its thumb at 50; anchoring it to a box that layout places at the value, under the layer's own anchor mode, also removes the component-authored positionTryFallbacks that spec:AST-059 FR4 forbids and the travel box held in React state.
  • test.use({video}) inside describe('drag clip') makes playwright test --list fail, so pr-a11y (browser specs) runs no spec; it needs file or project scope.
  • The changeset's claim that the tooltip sits over the thumb in every engine needs correcting with the fix.

[Automated review]

// keeps it in the viewport wherever the slider is.
valueTooltipHorizontal: {
translate: {
default: `clamp(0px, ${THUMB_INSET_PX}px + var(${TOOLTIP_PROGRESS}) * (100cqi - ${THUMB_SIZE}px) - 50%, 100cqi - 100%) 0`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

100cqi measures the travel box only while the tooltip is its descendant. layerHost.ts portals the layer beyond an unsafe ancestor such as ul, ol, table, label, a, button or span, and there 100cqi resolves against another container or the small viewport. On this head's Storybook build, a 500 px Slider in a list item or table cell shows its tooltip 406 px from the thumb at 50 and 802 px at 100. Anchoring to a box that layout places at the value keeps the placement independent of hosting.

// Only the placement-axis flip: the translate already keeps the tooltip
// inside the control on the other axis, and an alignment fallback would
// move the edge the translate starts from.
positionTryFallbacks: isHorizontal ? 'flip-block' : 'flip-inline',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-059 FR4: "The runtime authors both orders; a component passes no fallbacks of its own to an anchor-mode layer." useLayer spreads this style after its own positionTryFallbacks (var(<try-fallbacks property>, <standard list>)). This value therefore replaces both the standard order and the anchor-clear switch for every value tooltip.

const trackRef = useRef<HTMLDivElement>(null);
const draggingThumbRef = useRef<number | null>(null);
// The travel box: every part sits in it, and the value tooltips anchor to it.
const [travelBox, setTravelBox] = useState<HTMLDivElement | null>(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

architecture:react-component-runtime INV1: "DOM nodes and whole DOM collections MUST NOT be stored in React state." Holding the node here also renders every Slider twice on mount and needs the unnamed layout effect in SliderValueTooltip (INV3). A callback ref composed with positionRef avoids both.

});

test.describe('drag clip', () => {
test.use({video: {mode: 'on', size: {width: 800, height: 300}}});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Playwright rejects this: "Cannot use({ video }) in a describe group, because it forces a new worker." On this head playwright test --list --project chromium exits 1, so pr-a11y (browser specs) ran no spec, this pull request's geometry spec included. Move video to file or project scope.

'@astryxdesign/core': patch
---

[perf] Slider: the thumbs and the filled range move by `translate` in the control's container units instead of by inset and width, so moving the thumbs and the fill no longer lays out the page; with `valueDisplay="none"` a drag or step lays out nothing at all (the value tooltip and text display still lay out as their text changes). Rendering is unchanged, except in a left-to-right region inside a right-to-left page, where the thumb now sits on its value instead of one thumb width past it, and at the ends of a vertical slider, where the value tooltip now stays within the control. The value tooltip anchors to the slider's untransformed travel box and moves by the same `translate` as its thumb, so it sits over the thumb in every engine (engines disagree on whether CSS anchor positioning follows a `translate` on the anchor) with no added element, and inline padding on the control still moves no part. `useTooltip`'s `renderTooltip` now applies the `xstyle` and `style` its second argument already accepted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"Sits over the thumb in every engine" and "Rendering is unchanged" do not hold where the layer is portaled, such as a list, table, label, link, button or span. Please revise with the tooltip fix.

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:32
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
# Conflicts:
#	.github/workflows/ci.yml
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
meta#8320), meta#8189 at ca43cee, meta#8168 at 1cb400e

# Conflicts:
#	packages/cli/test/__golden__/no-icon-set/search-icon-set.json
#	packages/cli/test/__golden__/no-icon-set/search-icon-set.txt

This branch was successfully deployed

1 active deployment
Preview — 4733fa01 Deployed Oct 28, 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants