From d6ed165ac68c4e17808b18296da038f6408f0578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 22 Oct 2026 17:02:26 +0200 Subject: [PATCH] fix(ui): open the bottom sheet without any slide under reduced motion data-[state=open]:animate-in carries an attribute selector and outranked motion-reduce:animate-none, so the slide stayed, only cut to 0.01 ms by the global reduced-motion rule. It still existed and ran until the next drawn frame. The none is important now, on the sheet and the backdrop. --- src/components/ui/__tests__/bottom-sheet.test.tsx | 6 +++++- src/components/ui/bottom-sheet.tsx | 10 +++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/ui/__tests__/bottom-sheet.test.tsx b/src/components/ui/__tests__/bottom-sheet.test.tsx index 68baef3cb..ad8fca993 100644 --- a/src/components/ui/__tests__/bottom-sheet.test.tsx +++ b/src/components/ui/__tests__/bottom-sheet.test.tsx @@ -74,6 +74,9 @@ describe("", () => { body, ); expect(attr(html, "sheet-overlay")).toContain("touch-none"); + expect(attr(html, "sheet-overlay")).toContain( + "motion-reduce:animate-none!", + ); }); it("drops the grip, the drag and the X offset when it is not draggable", () => { @@ -123,7 +126,8 @@ describe("", () => { expect(BOTTOM_SHEET_CLASS).toContain("data-[state=open]:duration-260"); expect(BOTTOM_SHEET_CLASS).toContain("data-[state=closed]:duration-220"); expect(BOTTOM_SHEET_CLASS).toContain("ease-out"); - expect(BOTTOM_SHEET_CLASS).toContain("motion-reduce:animate-none"); + // Important: the data-state animation outranks a plain media variant. + expect(BOTTOM_SHEET_CLASS).toContain("motion-reduce:animate-none!"); expect(BOTTOM_SHEET_CLASS).toContain("max-h-[90dvh]"); }); }); diff --git a/src/components/ui/bottom-sheet.tsx b/src/components/ui/bottom-sheet.tsx index e684707a1..b63d29f9e 100644 --- a/src/components/ui/bottom-sheet.tsx +++ b/src/components/ui/bottom-sheet.tsx @@ -59,15 +59,19 @@ export type BottomSheetContentProps = React.ComponentProps< /** * No `bottom-*` here: the sheet's `bottom` comes from the keyboard rule in * `globals.css`, which a utility would outrank. In 260 ms, out 220 ms, both - * easing out; no slide at all under reduced motion. The `after:` strip + * easing out; no slide at all under reduced motion. That `animate-none` is + * important: `data-[state=open]:animate-in` carries an attribute selector and + * outranks a plain media variant, so without the `!` the slide stayed on, + * only shortened to 0.01 ms by the global reduced-motion rule, and still + * existed (and ran) until the next frame was drawn. The `after:` strip * continues the sheet's own background below its edge, so the short * resisting travel above the top never shows the page underneath. */ export const BOTTOM_SHEET_CLASS = - "bg-background fixed inset-x-0 z-50 flex max-h-[90dvh] flex-col gap-4 border-t shadow-lg transition-transform ease-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom data-[state=open]:duration-260 data-[state=closed]:duration-220 motion-reduce:animate-none motion-reduce:transition-none after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-10 after:bg-inherit after:content-['']"; + "bg-background fixed inset-x-0 z-50 flex max-h-[90dvh] flex-col gap-4 border-t shadow-lg transition-transform ease-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom data-[state=open]:duration-260 data-[state=closed]:duration-220 motion-reduce:animate-none! motion-reduce:transition-none after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-10 after:bg-inherit after:content-['']"; const OVERLAY_CLASS = - "fixed inset-0 z-50 touch-none bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 ease-out data-[state=open]:duration-260 data-[state=closed]:duration-220 motion-reduce:animate-none"; + "fixed inset-0 z-50 touch-none bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 ease-out data-[state=open]:duration-260 data-[state=closed]:duration-220 motion-reduce:animate-none!"; /** Puts a preview sheet back on its preview step each time it opens. */ function OnOpen({ run }: { run: () => void }) {