Skip to content

fix(core): a control's press release runs on the compositor - #9783

Draft
cixzhang wants to merge 1 commit into
fix/press-touch-down-highlightfrom
fix/control-press-release-compositor
Draft

cixzhang wants to merge 1 commit into
fix/press-touch-down-highlightfrom
fix/control-press-release-compositor

Conversation

@cixzhang

@cixzhang cixzhang commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Supersedes #8118 by @vjeux, rebased onto main and stacked on #9765 (#8117's replacement). The change is vjeux's: his commit authorship is kept, and the changeset credits him. This pull request targets #9765's branch so only its own change shows.

Draft until an owner decision. component:Button FR15's release length waits on Cindy's decision (Specs & Decisions' batch card 47). Until then, on Specs' direction, the compositor layers play the strength's own press clock and curve, main's tempo-scaled 200 ms deselect fade. Card 47's answer is then a change to one constant, CONTROL_RELEASE. Specs will give the FR15 text and stamp after that decision.

User impact

This affects people who tap a button or a card on a touch-first device: Button, IconButton, ToggleButton, ClickableCard, SelectableCard and Thumbnail.

  • Today: a tap usually starts work, so the main thread is busiest just after the lift. The release animates a gradient strength that only the main thread can repaint, so the press stays at full strength while script runs, then snaps off.
  • After this change: under a coarse primary pointer the release is an opacity animation, which engines run on the compositor. It keeps fading while script runs, and paints nothing per frame.
  • Unchanged: any other pointer, a finger on a touch screen beside a mouse included, keeps the press it has today: no layer, no isolation, the strength's own release.

Problem and solution fit

  1. Why it occurs: the press strength is a custom property feeding a gradient, repainted on the main thread.
  2. Why it harms: a tap starts work, so the thread is busiest then.
  3. Why it matters: the release confirms the tap; frozen, it reads as lag.

Under a coarse primary pointer, Button paints the press on its own ::before (controlPressLayerStyles.control). The layer, isolation and a will-change: transform exist only while a press is on or fading. The cards and Thumbnail fade their own ::after (controlOver). #9758's controller ends the press when that layer's release animation has played.

Changes since #8118

main and Specs & Decisions' rulings changed what this pull request carries:

Expected behavior and authority

Cindy's 10-25 ruling allows Button's ::before on touch. spec:AST-094 FR5 requires the compositor release. design:user-states DR1 and DR5. A fine pointer is unchanged (spec:AST-017 FR59). component:Button FR15's text waits on card 47.

Verification

  • Unit, red first: 5 cases fail on fix(core): under a finger a pressable highlights at touch-down, and waits only inside a scroller #9765's head and pass here:
    • Button's touch press is on a compositor layer that releases on the press clock;
    • Button has no layer and no isolation at rest;
    • ClickableCard, SelectableCard and Thumbnail release on their ::after.
  • Guards: a fine pointer keeps the strength's release (keepsPressOutsideCoarse), and an Item row keeps its layer to a press.
  • Suites: Button, IconButton, ToggleButton, the three cards, Thumbnail, Item, utils and the stylesheet test pass. So do lint, the core type check, the private-attribute baseline check, check:changesets and check:knowledge. review:pre reports no findings (1,779 authored lines).
  • Real Chromium 154, phone viewport with touch: the Button and List press interaction specs (ButtonPress, ButtonPressRelease, ButtonPressTiming, ListRowPress, ListRowPressPaths) pass 39, with 1 skipped. On fix(core): under a finger a pressable highlights at touch-down, and waits only inside a scroller #9765's head, 8 ButtonPressRelease cases fail:
    • the frame-by-frame release curve on a button and on a card;
    • Reduce Motion's single opacity cross-fade on a button and on a card;
    • no layer at rest;
    • the release keeping fading through a blocked main thread;
    • the busy-first-frame start;
    • the storyboard.
  • Press accessibility contracts (PressModality, PressedState, CardPress): 16 of 17 pass. PressModality's mouse-drag-across-menu-rows case fails intermittently in my rig on fix(core): under a finger a pressable highlights at touch-down, and waits only inside a scroller #9765's head too, and passed 4 of 4 runs earlier.
  • WebKit: not available in my local rig; CI runs it.

Scope

  • One defect is restored.
  • No new public API; the release keeps its clock and curve.
  • Public text and artifacts contain no internal Meta context.

@vercel

vercel Bot commented Nov 3, 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 Nov 3, 2026 7:01pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 3, 2026, 7:11 PM UTC

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 3, 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.

The compositor release works as described in Chromium and keeps its clock and curve. Approval waits on one spec decision and three fixes.

Spec decision needed: no current record says Button may take its own ::before during a touch press, and that changes what a caller's ::before styles render. An engineering owner needs to provide or identify the governing clause (for example in component:Button) and records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner.

Required fixes:

  • Nest willChange under @media (pointer: coarse) in controlPressLayerStyles.control (privateAttributes/press.stylex.ts:302), because a pen or finger press on a fine-pointer device now gets will-change: transform, against spec:AST-017 FR59 and the changeset's "nothing changes".
  • Drop expect(state.press).toBeNull() from the fine-pointer mouse test (ButtonPressRelease.interaction.browser.spec.ts:376), because component:Button FR15 routes a mouse press through the press controller and the assertion pins that recorded gap as a rule.
  • Build Button's layer from one shared press-layer style with the rows instead of copying rowPressLayerStyles.cell and cellOnImage, keeping only Button's own coarse-guarded willChange (and later its clock) in control.

[Automated review]

// for it and drops that layer at its end, which repaints the button. A
// layer kept from the touch to the end of the release keeps the release
// free of Paint.
willChange: {default: null, [PRESSED_ANY]: '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 is the only control arm outside @media (pointer: coarse). The controller writes the press state for every non-mouse pointer, so on a fine-pointer device a pen or finger press now gets will-change: transform (a stacking context and a compositing layer for the press). That goes against spec:AST-017 FR59 and the changeset's "nothing changes".

willChange: {default: null, [COARSE]: {default: null, [PRESSED_ANY]: 'transform'}},

keepsPressOutsideCoarse could also reject will-change outside the coarse query, so the guard catches this.

expect(['none', 'normal'], JSON.stringify(state)).toContain(state.layer);
expect(state.isolation).toBe('auto');
// A mouse's press is `:active`'s: the controller writes nothing.
expect(state.press).toBeNull();

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:Button FR15 says a press paints "through the press controller: at once under a mouse or a pen", and Button's Current-state impact lists the mouse path as not yet implemented. This assertion turns that gap into "the desktop rule", so whoever implements the mouse path gets a failing test. The layer and isolation checks above already prove this test's point, so this line can go.

* rest, and with any other pointer, nothing here applies: no layer, no
* `isolation`, and the variant's own paint and release, as before.
*/
control: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

control repeats rowPressLayerStyles.cell and cellOnImage property for property: the image arms, the coarse animation-name: none, isolation, the ::before box, opacity, clock and fill mode. controlRelease duplicates cellRelease. Only the ENABLED nesting and willChange are new. StyleX merges ::before properties one at a time (as cellWithoutLayer relies on), so Button could compose one shared press-layer style with the rows and keep only its own coarse-guarded willChange, and later its clock, here.

Under a coarse primary pointer, Button (and IconButton and ToggleButton)
paints its touch press on its own ::before layer, present only while a
press is on or fading, with isolation only then. ClickableCard,
SelectableCard and Thumbnail fade their ::after. Each release is an
opacity animation, which engines run on the compositor, so it keeps
fading while the page's script runs. Any other pointer is unchanged.

Rebased onto main and #9765 (which supersedes #8117), on Specs'
direction: the layers play the strength's own press clock and curve
(main's tempo clock) for now, and a pen follows the touch presentation.
Button FR15's release length waits on an owner decision. So this drops
#8118's UIButton table, its mouse and key input marker, its #8350 CLI
files and #8338's reduced-motion arms. The press layer styles live in
the press attribute's owner module.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>

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

The compositor release works as described in Chromium and keeps its clock and curve. Approval waits on one spec decision and three fixes.

Spec decision needed: no current record says Button may take its own ::before during a touch press, and that changes what a caller's ::before styles render during a press (the press fill can disappear and the caller's decoration moves). An engineering owner needs to settle whether Button's component record claims that pseudo-element (or whether the press must leave a caller's ::before intact), and records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner.

Required fixes:

  • Nest willChange under @media (pointer: coarse) in controlPressLayerStyles.control (privateAttributes/press.stylex.ts:306), because a pen or finger press on a fine-pointer device now gets will-change: transform, against spec:AST-017 FR59 and the changeset's "nothing changes".
  • Drop expect(state.press).toBeNull() from the fine-pointer mouse test (ButtonPressRelease.interaction.browser.spec.ts:376), because spec:AST-094 FR11 and DEC-21 route a mouse press through the press controller and the assertion pins Button's recorded gap as a rule.
  • Build Button's layer from the shared row press-layer styles (rowPressLayerStyles.cell and cellOnImage) instead of copying them, keeping only Button's own coarse-guarded willChange (and later its clock) in control.

[Automated review]

// for it and drops that layer at its end, which repaints the button. A
// layer kept from the touch to the end of the release keeps the release
// free of Paint.
willChange: {default: null, [PRESSED_ANY]: '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 is the only control arm outside @media (pointer: coarse). The controller writes the press state for every non-mouse pointer, so on a fine-pointer device a pen or finger press now gets will-change: transform (a stacking context and a compositing layer), against spec:AST-017 FR59 and the changeset's "nothing changes". Suggest willChange: {default: null, [COARSE]: {default: null, [PRESSED_ANY]: 'transform'}}, and have keepsPressOutsideCoarse reject will-change outside the coarse query.

expect(['none', 'normal'], JSON.stringify(state)).toContain(state.layer);
expect(state.isolation).toBe('auto');
// A mouse's press is `:active`'s: the controller writes nothing.
expect(state.press).toBeNull();

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-094 FR11 says a fine pointer's press paint starts "through the same press controller" and DEC-21 says "The press controller tracks every pointer"; Button's record lists the mouse path as not yet implemented. This assertion pins that gap as a rule, so closing it fails this test. Suggest dropping it and keeping the layer and isolation checks.

* rest, and with any other pointer, nothing here applies: no layer, no
* `isolation`, and the variant's own paint and release, as before.
*/
control: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

control repeats rowPressLayerStyles.cell and cellOnImage property for property (image arms, coarse animation-name: none, isolation, the ::before box, opacity, clock and fill mode), and controlRelease duplicates cellRelease. Composing Button from those styles, with only Button's coarse-guarded willChange (and later a clock override) here, keeps one path for both.

This branch was successfully deployed

1 active deployment
Preview — 6e6dbb66 Deployed Nov 3, 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants