Skip to content

Overlay: animate backdrop opacity instead of background-color - #667

Merged
blvdmitry merged 3 commits into
mainfrom
claude/animation-performance-j38gm8-overlay
Sep 12, 2026
Merged

blvdmitry merged 3 commits into
mainfrom
claude/animation-performance-j38gm8-overlay

Conversation

@blvdmitry

@blvdmitry blvdmitry commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Following the guidance in The Browser's Main Thread Is Expensive: only transform and opacity can be animated on the compositor thread, everything else forces style/layout/paint work on the main thread every frame.

The Overlay root is a position: fixed element covering the whole viewport. It transitioned background-color (0% → 70% black) together with opacity on open/close, and Modal updated that background-color on every frame of the drag-to-close gesture through the inherited --rs-overlay-opacity custom property. Every one of those frames repainted the full viewport on the main thread, and the custom property change on the root also forced a style recalculation of the entire overlay subtree (the whole modal content).

This PR renders the backdrop as a dedicated .backdrop element with a static 70% black background and animates only its opacity:

  • Open/close: .backdrop transitions opacity with the same duration/easing as the root, so the resulting fade curve (0.7 · t²) is unchanged and runs on the compositor.
  • Drag-to-close: instanceRef.setOpacity() now writes style.opacity directly on the backdrop element, so the update is scoped to that single element instead of invalidating style for every descendant. Measured in Chromium: updating an inherited custom property on the root of a 6000-node subtree costs ~10ms of style recalc per update, versus ~0.01ms for a direct opacity write on one element.
  • The backdrop lives inside the wrapper (a display: table box that grows with the content) so it keeps covering the whole scrollable area when overflow="auto" is used by centered modals.
  • transparent and blurred variants keep working: transparent sets the backdrop opacity to 0 via the existing --click-through class, backdrop-filter stays on the root.
  • The internal --rs-overlay-opacity custom property is removed; the public instanceRef.setOpacity() API is unchanged.

Related Issue

N/A

Screenshots / Recordings

Verified in headless Chromium against Storybook: the blurred, transparent, and tall centered (scrolled overflow: auto) modal states are pixel-identical to main at rest, the backdrop still covers the viewport when the overlay is scrolled to the bottom, and a simulated touch drag on a bottom modal dims the backdrop (opacity 0.68 at 88px of drag) and closes on release.

Notes for Reviewers

  • Overlay and Modal story tests pass locally (23 tests).
  • The will-change: opacity on the backdrop keeps it promoted between drag updates so setOpacity() calls don't trigger repaints; the layer only exists while an overlay is rendered.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EAi9QQVSFuJ7Gkf8SUTYJq

The overlay root is a position: fixed element covering the viewport and
transitioned background-color together with opacity on open/close, and
updated background-color on every frame of the Modal drag-to-close
gesture. background-color changes are painted on the main thread, so
every frame repainted the whole viewport.

Move the backdrop to a ::before layer with a static 70% black background
whose opacity follows --rs-overlay-opacity. Opacity is a compositor-only
property, so the transition and the drag updates no longer trigger
paint. The pseudo element is attached to the wrapper (which grows with
the content) so it still covers the whole scrollable area when the
overlay has overflow: auto. Both the root and the backdrop keep the same
duration and easing, so the resulting fade curve is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAi9QQVSFuJ7Gkf8SUTYJq
@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

size-limit report 📦

Path Size Loading time (3g) Running time (snapdragon) Total time
Library / JS 50.66 KB (0%) 1.1 s (0%) 2.1 s (-7.88% 🔽) 3.1 s
Library / CSS 23.87 KB (0%) 478 ms (0%) 0 ms (+100% 🔺) 478 ms
Theming / JS 7.78 KB (0%) 156 ms (0%) 469 ms (+293.9% 🔺) 625 ms
Theming with a default theme definition / JS 8.69 KB (0%) 174 ms (0%) 279 ms (+65.24% 🔺) 453 ms

Driving the backdrop opacity through an inherited custom property set
on the root made Chromium recalculate style for the whole overlay
subtree (the entire modal content) on every drag frame. The backdrop is
now a real element and setOpacity writes its opacity directly, so the
recalculation stays scoped to that single element. The internal
--rs-overlay-opacity custom property is removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EAi9QQVSFuJ7Gkf8SUTYJq
@blvdmitry
blvdmitry marked this pull request as ready for review September 12, 2026 16:19
@blvdmitry
blvdmitry merged commit 3d94de2 into main Sep 12, 2026
7 of 8 checks passed
@blvdmitry
blvdmitry deleted the claude/animation-performance-j38gm8-overlay branch September 12, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants