diff --git a/.changeset/overlay-backdrop-compositor.md b/.changeset/overlay-backdrop-compositor.md new file mode 100644 index 00000000..b401441a --- /dev/null +++ b/.changeset/overlay-backdrop-compositor.md @@ -0,0 +1,5 @@ +--- +"reshaped": patch +--- + +Improved the `Overlay` animation performance by rendering its backdrop as a separate layer with a static color and transitioning only its `opacity`. Previously the full-viewport root transitioned `background-color` on open and close and during the `Modal` drag-to-close gesture, which forced the browser to repaint the whole viewport on every frame on the main thread. The transition now runs entirely on the compositor thread while keeping the same visual result. diff --git a/packages/reshaped/src/components/Overlay/Overlay.module.css b/packages/reshaped/src/components/Overlay/Overlay.module.css index 46fd3aa7..39137577 100644 --- a/packages/reshaped/src/components/Overlay/Overlay.module.css +++ b/packages/reshaped/src/components/Overlay/Overlay.module.css @@ -2,7 +2,6 @@ /* Add an offset in case overlay is rendered inside a scrolled container */ --rs-overlay-offset-y: 0px; --rs-overlay-offset-x: 0px; - --rs-overlay-opacity: 0; position: fixed; overflow-x: clip; @@ -15,7 +14,6 @@ width: 100%; z-index: var(--rs-z-index-fixed); color: var(--rs-color-white); - background-color: rgb(from var(--rs-color-black) r g b / 0%); opacity: 0; outline: none; isolation: isolate; @@ -25,6 +23,17 @@ display: table; height: 100%; width: 100%; + position: relative; +} + +/* Setting opacity separately for swipe updates directly in the dom */ +.backdrop { + position: absolute; + inset: 0; + z-index: -1; + background-color: rgb(from var(--rs-color-black) r g b / 70%); + opacity: 0; + will-change: opacity; } .inner { @@ -40,15 +49,20 @@ } .root.--visible { - --rs-overlay-opacity: 1; - - background-color: rgb(from var(--rs-color-black) r g b / calc(var(--rs-overlay-opacity) * 0.7)); opacity: 1; + + & .backdrop { + opacity: 1; + } } .root.--click-through { color: inherit; pointer-events: none; + + & .backdrop { + opacity: 0; + } } .root.--blurred { @@ -74,11 +88,18 @@ } .root.--animated { - transition: var(--rs-duration-medium) var(--rs-easing-accelerate); - transition-property: background-color, opacity; + transition: opacity var(--rs-duration-medium) var(--rs-easing-accelerate); + + & .backdrop { + transition: opacity var(--rs-duration-medium) var(--rs-easing-accelerate); + } &.--visible { transition-timing-function: var(--rs-easing-decelerate); + + & .backdrop { + transition-timing-function: var(--rs-easing-decelerate); + } } } diff --git a/packages/reshaped/src/components/Overlay/Overlay.tsx b/packages/reshaped/src/components/Overlay/Overlay.tsx index 303effad..f2a37fee 100644 --- a/packages/reshaped/src/components/Overlay/Overlay.tsx +++ b/packages/reshaped/src/components/Overlay/Overlay.tsx @@ -42,6 +42,7 @@ const Overlay: React.FC = (props) => { const [animated, setAnimated] = React.useState(false); const [offset, setOffset] = React.useState([0, 0]); const rootRef = React.useRef(null); + const backdropRef = React.useRef(null); const scopeRef = React.useRef(null); const contentRef = React.useRef(null); const { lockScroll, unlockScroll } = useScrollLock({ containerRef }); @@ -183,7 +184,8 @@ const Overlay: React.FC = (props) => { () => ({ setOpacity: (value: number) => { if (transparent) return; - rootRef.current?.style.setProperty("--rs-overlay-opacity", `${value}`); + // Applied directly to the backdrop element to keep the style recalculation scoped to it + backdropRef.current?.style.setProperty("opacity", `${value}`); }, }), [transparent] @@ -205,7 +207,6 @@ const Overlay: React.FC = (props) => { { "--rs-overlay-offset-x": containerRef ? `${offset[0]}px` : undefined, "--rs-overlay-offset-y": containerRef ? `${offset[1]}px` : undefined, - "--rs-overlay-opacity": transparent ? 0 : undefined, } as React.CSSProperties } // oxlint-disable-next-line jsx_a11y/prefer-tag-over-role @@ -217,6 +218,7 @@ const Overlay: React.FC = (props) => { onTransitionEnd={handleTransitionEnd} >
+
{typeof children === "function" ? children({ active: visible }) : children}