Skip to content

fix(Layer): under a finger with Reduce Motion, a layer fades in instead of appearing at once - #8214

Closed
vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/c26-touch-reduced-motion
Closed

vjeux wants to merge 1 commit into
meta:mainfrom
vjeux:uikit/c26-touch-reduced-motion

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

User impact

With Reduce Motion on, a Tooltip, HoverCard, menu or other layer that opens under a finger appeared in a single frame: the shared layer entrance dropped its whole animation, fade included. On a touch screen Reduce Motion replaces motion with a cross-fade, never a jump, so the layer should fade in where it lands.

Evidence

iOS 26.2 Safari with Reduce Motion on, a tap on the Tooltip's term (a grey dot drawn by the test page marks the finger); before (main), then after:

Before After

  • Before: the tooltip appears in a single frame of the 60 fps recording.
  • After: it fades in where it lands over about 115 ms (7 frames), with no travel and no scale. Under a mouse with Reduce Motion layers still have no animation; under a finger without it the entrance still animates opacity and transform.
  • Regression proof: both "fades in" cases of the Chromium contract fail on main's build.

Problem and solution fit

layerAnimations (the entrance every layer applies through layer.render) set animation-name: none under prefers-reduced-motion: reduce. That suits a mouse, where the theme decides. Under a coarse pointer it is a jump where iOS fades: a menu, a popover or a tip with Reduce Motion lands in place and fades in, with no travel and no scale.

Under a coarse pointer with Reduce Motion the entrance now keeps its opacity ramp and drops only the slide and the scale. Under a mouse it still appears at once, and without Reduce Motion nothing changes. Components with their own touch presentation (DropdownMenu, Popover in the open stacks) override this entrance inline, so this only reaches layers that have none: Tooltip, HoverCard, Selector, ContextMenu and the like.

Expected behavior and authority

  • design:motion DR5 (current): a reduced-motion path communicates the same result without travel.
  • Touch presentations substitute the way UIKit does, an entrance landing in place and fading in (AST-094 FR8, draft in spec(AST-094): touch presentations follow UIKit #8052).

Smallest restoration

One added media condition per placement in layerAnimations.stylex.ts (a fade keyframe), and a real-engine contract with its own pr-a11y step, placed where none of the 130 open pull requests touching ci.yml adds a conflict. The fade keeps the theme's entrance timing; a UIKit-timed touch entrance for tooltips is an open owner question (D7, InfoTip as a TipKit popover tip), not built here.

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.

Testing

  • pnpm exec playwright test packages/core/src/Layer/__tests__/LayerReducedMotion.a11y.chromium.spec.ts --project chromium after pnpm storybook:build: 6/6 (Tooltip and HoverCard: fade under a finger with Reduce Motion, unchanged without it, still instant under a mouse); the two fade cases fail against main.
  • pnpm vitest run .github/scripts/ci-test-routing.test.mjs .github/scripts/release-check-workflow.test.mjs: 101 pass.
  • pnpm vitest run packages/core/src/Layer packages/core/src/Tooltip packages/core/src/HoverCard packages/core/src/Popover packages/core/src/DropdownMenu packages/core/src/Selector packages/core/src/ContextMenu plus the build-css graph, layers and data-token tests: 1001 pass.
  • iOS 26.2 simulator in Safari with Reduce Motion on, before and after, 60 fps recordings with a page log.
  • Core typecheck, eslint, prettier.

…ad of appearing at once

The shared layer entrance (layerAnimations) dropped its animation under
prefers-reduced-motion, so a Tooltip, HoverCard or menu opened by a tap
appeared in one frame. On a touch screen Reduce Motion replaces motion with
a cross-fade, never a jump: under a coarse pointer the entrance now keeps
its opacity ramp and drops only the travel and the scale. Under a mouse it
still appears at once, and without Reduce Motion nothing changes.

LayerReducedMotion.a11y.chromium.spec.ts proves the three cases in a real
engine (media queries), with its own pr-a11y step.
@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 24, 2026 11:38am 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
@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 24, 2026, 12:03 PM UTC

@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

No new or modified components detected.

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.9KB 1.2KB

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

46 of 494 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
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
RichTextEditor Markdown parity: side by side probe dark 94,866
RichTextEditor Markdown parity: side by side probe light 89,762
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
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
ProgressBar With Target Mark probe dark 7,895
ProgressBar With Target Mark probe light 7,895
Markdown Default neutral dark 7,573
Markdown Default neutral light 7,566

and 26 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.

Approval of this change waits on an engineering owner's decision, because the behaviour it adds is not yet governed by a current spec. Fading layers in under a coarse pointer with Reduce Motion is not covered by any current record: design:motion (including DR5) is still draft, and AST-094 FR8 is only proposed in #8052. An engineering owner needs to decide whether every layer that uses layerAnimations should fade in (opacity only, with the theme's entrance timing) when Reduce Motion is on and (pointer: coarse) matches, or point to the current claim that governs it; they record approval by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Nothing else needs to change: the new contract fails on main's build and passes on this head, and the mouse and no-preference paths are unchanged.

[Automated review]

cixzhang added a commit that referenced this pull request Oct 24, 2026
…ers change at once (#8272)

## Exact decision needed

What a person who prefers reduced motion sees when a core component animates a change, on touch and on other pointers, and whether a component may keep its motion.

## Existing authority searched

- `design:motion` DR5 (draft): every animated transition has an immediate or minimally moving form. Draft, so not authority.
- `spec:AST-013` FR13: reduced-motion behavior is unchanged by server-rendered entrances; it defines none.
- `spec:AST-012` FR2: `motion: reduce` and `pointer: coarse/fine` are the adaptation conditions.
- No current web record defines component behavior under reduced motion.
- Search terms: reduced motion, prefers-reduced-motion, cross-fade, Reduce Motion, motion DR5, spring, travel. Overlapping open pull requests: #7367 (draft `design:motion` DEC-2, hover entrances; no reduced-motion rule), #8214 (layer fade under a finger with Reduce Motion; conforms once its fade is 200 ms or less), #8052 (AST-094 FR8 carries the matching touch rule), #8211, #8239, #8253 (touch indicator and placeholder work; continuous indicators are outside this record).

## Decision

New current record `design:reduced-motion`, matching iOS Reduce Motion with Prefer Cross-Fade:

- DR1: under `prefers-reduced-motion: reduce`, a core component's entrances, exits, presentation switches, and state changes never move, scale, resize, or spring.
- DR2: on a coarse pointer, they cross-fade: opacity only, 200 ms or less.
- DR3: on any other pointer, they complete at once.
- DR4: no core component, prop, or theme value keeps motion under reduced motion.

`spec:AST-057` FR5 names that a swiped row's slide out, spring back, and hold-out follow DR2 under reduced motion. The draft `design:motion` keeps its other rules unapproved; its DR5 points to the new record. A separate record keeps the promotion to this one rule, as `docs/design/README.md` advises for a different approval lifecycle.

## Rejected alternatives

- A per-component setting that keeps full motion under reduced motion.

## Claim scope and non-goals

Only transitions under reduced motion. Motion without the preference, durations and easing, tokens, continuous indicators such as a spinner, and content the person drags with their own finger are outside this record.

## Evidence and implementation relationship

Layers that open under a finger with Reduce Motion appear in one frame today, where iOS cross-fades. The open fix #8214 makes them fade under a coarse pointer and stay immediate under a mouse, which conforms to DR2 and DR3 once its fade is 200 ms or less. The open draft #7367 (`design:motion` DEC-2, hover-summoned layers) does not touch reduced motion. The AST-094 revision in #8052 carries the matching touch rule. No implementation is included.

## Writing rubric results

- R1 narration: none.
- R2 research in the record: none.
- R3 evidence naming a change: none; verification names browser states.
- R4 rejected alternatives over one line or outside a decision: one, inside DEC-1, one line.
- R5 pull request or issue references remaining: none.
- R6 ruled open questions still listing options: none.

## Scope

- [x] This PR contains specification records only.
- [x] It records observable behavior and preserves equivalent internal implementations.
- [x] Any named internal mechanism is intentionally public, names its dependent caller/system, and explains why an equivalent implementation would not satisfy the contract.
- [x] Verification states the evidence layer and failure signal without prescribing CI job/workflow topology.
- [x] It records one intentional durable decision, not a rescue for a separable tagalong.
- [x] The canonical knowledge owner, affected current claims, and overlapping open work are named; any ownership collision is resolved or called out for human decision.
- [x] Adjacent API, visual, accessibility, composition, and implementation facts remain outside scope unless this exact decision depends on them.
- [x] Public text and artifacts contain no internal Meta context.

## Validation

- `pnpm check:knowledge` (CI lint)
- Prettier
- `git diff --check`
@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

This was closed by GitHub's closing-keyword parsing, not by a review: the squash commit of #8272 says The open fix #8214 makes them fade…, and GitHub reads fix #8214 as a closing reference. #8272 itself describes this change as conforming to design:reduced-motion DR2 and DR3 once its fade is 200 ms or less.

The branch now caps the touch cross-fade at 200 ms (min(--duration-fast-max, 200ms); 165 ms with the default motion scale), with a Chromium case that proves the cap, so it conforms to DR2 (opacity only, 200 ms or less under a coarse pointer) and DR3 (at once under a mouse). This pull request can't be reopened from the author's side, so the same branch continues in #8322.

cixzhang pushed a commit that referenced this pull request Oct 25, 2026
…ad of appearing at once (#8322)

Supersedes #8214, which #8272's merge closed through its closing keyword; same branch, plus the 200 ms cap that `design:reduced-motion` asks for.

## User impact

With Reduce Motion on, a Tooltip, HoverCard, menu or other layer that opens under a finger appeared in a single frame: the shared layer entrance dropped its whole animation, fade included. On a touch screen Reduce Motion replaces motion with a cross-fade, never a jump.

## Evidence

iOS 26.2 Safari with Reduce Motion on, a tap on the Tooltip's term (a grey dot drawn by the test page marks the finger); before (main), then after:

![Before](https://raw.githubusercontent.com/vjeux/astryx/5d92dd599ea0966754547e01fdb59e1d8e86acb5/ios-reduce-motion-tooltip-before.gif) ![After](https://raw.githubusercontent.com/vjeux/astryx/5d92dd599ea0966754547e01fdb59e1d8e86acb5/ios-reduce-motion-tooltip-after.gif)

- Before: the tooltip appears in a single frame of the 60 fps recording.
- After: it fades in where it lands, opacity only, over the theme's fast duration (165 ms with the default motion scale; most of the change in the first 115 ms on the standard ease). Under a mouse with Reduce Motion it still appears at once; under a finger without it the entrance still animates opacity and transform.
- Regression proof: the Chromium contract's fade cases fail on main's build, and its slow-theme case (`--duration-fast-max: 400ms`) fails without the cap (0.4 s) and passes with it (0.2 s).

## Problem and solution fit

`layerAnimations` (the entrance every layer applies through `layer.render`) set `animation-name: none` under `prefers-reduced-motion: reduce`. That suits a mouse. Under a coarse pointer it is a jump where iOS fades.

Under a coarse pointer with Reduce Motion the entrance now keeps its opacity ramp, drops the slide and the scale, and lasts `min(--duration-fast-max, 200ms)`. Under a mouse it still appears at once; without Reduce Motion nothing changes. Components with their own touch presentation (DropdownMenu, Popover in the open stacks) override this entrance inline, so this reaches layers that have none: Tooltip, HoverCard, Selector, ContextMenu and the like.

## Expected behavior and authority

- `design:reduced-motion` (current): under reduced motion nothing moves, scales or springs (DR1); on a coarse pointer it cross-fades, opacity only, in 200 ms or less (DR2); on other pointers it changes at once (DR3); no theme value keeps motion (DR4).

## Smallest restoration

One added media condition per placement in `layerAnimations.stylex.ts` (a fade keyframe, and the capped duration), and a real-engine contract with its own pr-a11y step. A UIKit-timed touch entrance for tooltips is an open owner question (D7), not built here.

## Scope

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

## Testing

- `pnpm exec playwright test packages/core/src/Layer/__tests__/LayerReducedMotion.a11y.chromium.spec.ts --project chromium` after `pnpm storybook:build`: 8/8 (Tooltip and HoverCard: an opacity-only fade of 200 ms or less under a finger with Reduce Motion, also with a slow theme; unchanged without it; still instant under a mouse). The fade cases fail against main; the slow-theme case fails against #8214's head.
- `pnpm vitest run .github/scripts/ci-test-routing.test.mjs .github/scripts/release-check-workflow.test.mjs`: 101 pass.
- `pnpm vitest run packages/core/src/Layer packages/core/src/Tooltip packages/core/src/HoverCard packages/core/src/Popover packages/core/src/DropdownMenu packages/core/src/Selector packages/core/src/ContextMenu`: 983 pass.
- iOS 26.2 simulator in Safari with Reduce Motion on, before and after, 60 fps recordings with a page log; the cap changes nothing at the default theme's 165 ms.
- Core typecheck, eslint, prettier.

This branch was successfully deployed

1 active deployment
Preview — b6516e30 Deployed Oct 24, 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.

1 participant