diff --git a/apps/www/src/content/docs/components/accordion/index.mdx b/apps/www/src/content/docs/components/accordion/index.mdx index d052c8ba6..50c56b11d 100644 --- a/apps/www/src/content/docs/components/accordion/index.mdx +++ b/apps/www/src/content/docs/components/accordion/index.mdx @@ -90,3 +90,4 @@ The accordion content can contain any React elements, allowing for rich layouts - Trigger elements use `aria-expanded` to indicate open/closed state - Supports keyboard navigation with Enter and Space to toggle items - Content panels use `aria-controls` and `aria-labelledby` for association +- Respects motion preferences: icon rotation and panel expand/collapse motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/alert-dialog/index.mdx b/apps/www/src/content/docs/components/alert-dialog/index.mdx index 020854f7e..0230cdad6 100644 --- a/apps/www/src/content/docs/components/alert-dialog/index.mdx +++ b/apps/www/src/content/docs/components/alert-dialog/index.mdx @@ -121,3 +121,4 @@ You can nest alert dialogs for multi-step confirmation flows. When a nested aler - Uses `aria-label` or `aria-labelledby` to identify the dialog - Uses `aria-describedby` to provide additional context - Focus is trapped within the alert dialog while open +- Respects motion preferences: alert dialog panel motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/button/index.mdx b/apps/www/src/content/docs/components/button/index.mdx index c49f24a0e..96b1afc30 100644 --- a/apps/www/src/content/docs/components/button/index.mdx +++ b/apps/www/src/content/docs/components/button/index.mdx @@ -75,3 +75,4 @@ The button component accepts optional leading and/or trailing icons. - Supports keyboard activation with Enter and Space keys - Disabled state is communicated via `aria-disabled` attribute - Loading state prevents interaction and announces status to screen readers +- Respects motion preferences: button loader rotation is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/collapsible/index.mdx b/apps/www/src/content/docs/components/collapsible/index.mdx index 540f7abbe..1c0a72506 100644 --- a/apps/www/src/content/docs/components/collapsible/index.mdx +++ b/apps/www/src/content/docs/components/collapsible/index.mdx @@ -72,3 +72,4 @@ The collapsible can be disabled to prevent user interaction. - The Trigger uses `aria-expanded` to indicate the open/closed state of the panel. - Supports keyboard interaction with Enter and Space to toggle the panel. - The Panel is automatically associated with the Trigger via `aria-controls`. +- Respects motion preferences: panel expand/collapse motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/dialog/index.mdx b/apps/www/src/content/docs/components/dialog/index.mdx index 86e5d37ca..286b1fc36 100644 --- a/apps/www/src/content/docs/components/dialog/index.mdx +++ b/apps/www/src/content/docs/components/dialog/index.mdx @@ -123,3 +123,4 @@ You can nest dialogs within one another. When a nested dialog opens, the parent - Dialog has `role="dialog"` and `aria-modal="true"` - Uses `aria-label` or `aria-labelledby` to identify the dialog - Uses `aria-describedby` to provide additional context +- Respects motion preferences: dialog panel motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/drawer/index.mdx b/apps/www/src/content/docs/components/drawer/index.mdx index a54633317..3ef11853e 100644 --- a/apps/www/src/content/docs/components/drawer/index.mdx +++ b/apps/www/src/content/docs/components/drawer/index.mdx @@ -80,4 +80,5 @@ The Drawer can slide in from different sides of the screen. Swipe-to-dismiss is - Focus is trapped within the drawer and restored on close - Supports dismissal with Escape key and swipe gestures - Title is announced via `aria-labelledby` +- Respects motion preferences: drawer slide motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/link/index.mdx b/apps/www/src/content/docs/components/link/index.mdx index f994f6568..84f79b75a 100644 --- a/apps/www/src/content/docs/components/link/index.mdx +++ b/apps/www/src/content/docs/components/link/index.mdx @@ -65,3 +65,4 @@ The Link component follows accessibility best practices: - External links have aria-labels indicating they open in new tabs - Download links include appropriate aria-labels - Maintains color contrast ratios for all variants +- Hover feedback uses an opacity transition diff --git a/apps/www/src/content/docs/components/popover/index.mdx b/apps/www/src/content/docs/components/popover/index.mdx index dac86471c..bfba4b68d 100644 --- a/apps/www/src/content/docs/components/popover/index.mdx +++ b/apps/www/src/content/docs/components/popover/index.mdx @@ -61,3 +61,4 @@ Customize how the popover aligns with its trigger. - Trigger uses `aria-haspopup` and `aria-expanded` attributes - Focus is managed when opening and closing the popover - Supports dismissal with Escape key +- Respects motion preferences: popover entry motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/preview-card/index.mdx b/apps/www/src/content/docs/components/preview-card/index.mdx index 4b6cc6e6c..36d4933f2 100644 --- a/apps/www/src/content/docs/components/preview-card/index.mdx +++ b/apps/www/src/content/docs/components/preview-card/index.mdx @@ -76,3 +76,4 @@ Control the position of the preview card relative to its trigger. - Closes when the pointer leaves the trigger or content area - Content is accessible to screen readers when open - Trigger renders as a semantic `` element +- Respects motion preferences: preview card open/close motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/sidebar/index.mdx b/apps/www/src/content/docs/components/sidebar/index.mdx index 7b58eb0d5..636e4802d 100644 --- a/apps/www/src/content/docs/components/sidebar/index.mdx +++ b/apps/www/src/content/docs/components/sidebar/index.mdx @@ -103,7 +103,7 @@ Set `hideCollapsedItemTooltip` to disable tooltips on navigation items when the The Sidebar implements the following accessibility features: -- **Reduced motion** — Respects the user's motion preferences. When the system "Reduce motion" setting is enabled, collapse/expand and hover transitions are disabled so the sidebar updates without animation. +- **Reduced motion** — Sidebar collapse/expand motion is enabled only when `prefers-reduced-motion: no-preference`. - Proper ARIA roles and attributes diff --git a/apps/www/src/content/docs/components/skeleton/index.mdx b/apps/www/src/content/docs/components/skeleton/index.mdx index f8b824814..cad32ddc0 100644 --- a/apps/www/src/content/docs/components/skeleton/index.mdx +++ b/apps/www/src/content/docs/components/skeleton/index.mdx @@ -98,5 +98,5 @@ The Skeleton component follows accessibility best practices: - Uses semantic HTML elements - Provides appropriate ARIA attributes - Maintains sufficient color contrast -- Animation can be disabled for users who prefer reduced motion +- Respects motion preferences: skeleton shimmer is enabled only when `prefers-reduced-motion: no-preference` - Supports both block and inline layouts diff --git a/apps/www/src/content/docs/components/spinner/index.mdx b/apps/www/src/content/docs/components/spinner/index.mdx index c573fe3e1..e932f3add 100644 --- a/apps/www/src/content/docs/components/spinner/index.mdx +++ b/apps/www/src/content/docs/components/spinner/index.mdx @@ -44,3 +44,4 @@ The Spinner component includes appropriate ARIA attributes for accessibility: - `role="status"`: Indicates that the element is a status indicator. - `aria-hidden="true"`: Hides the spinner from screen readers, as it's a visual indicator only. +- Respects motion preferences: spinner rotation is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/tabs/index.mdx b/apps/www/src/content/docs/components/tabs/index.mdx index 4098d1ba7..f775f3820 100644 --- a/apps/www/src/content/docs/components/tabs/index.mdx +++ b/apps/www/src/content/docs/components/tabs/index.mdx @@ -85,4 +85,4 @@ Renders the content panel for a tab. - Uses `role="tablist"`, `role="tab"`, and `role="tabpanel"` - Active tab is indicated with `aria-selected` - Leading icon is wrapped with `aria-hidden` so it is not announced (treated as decorative) -- Respects `prefers-reduced-motion: reduce`: the tab indicator does not animate when the user has requested reduced motion in OS/browser settings +- Respects motion preferences: tab indicator motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/toast/index.mdx b/apps/www/src/content/docs/components/toast/index.mdx index d53884bdf..17962298f 100644 --- a/apps/www/src/content/docs/components/toast/index.mdx +++ b/apps/www/src/content/docs/components/toast/index.mdx @@ -95,3 +95,4 @@ Create a toast, then update or close it programmatically using the returned ID. - Close button has `aria-label="Close toast"` - Supports keyboard navigation and Escape to dismiss - Swipe-to-dismiss gesture support +- Respects motion preferences: toast enter/exit motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/apps/www/src/content/docs/components/tooltip/index.mdx b/apps/www/src/content/docs/components/tooltip/index.mdx index 2c579400e..7198906f5 100644 --- a/apps/www/src/content/docs/components/tooltip/index.mdx +++ b/apps/www/src/content/docs/components/tooltip/index.mdx @@ -83,3 +83,7 @@ Use `trackCursorAxis` prop on the Root component to make the tooltip follow the Show the arrow by setting `showArrow={true}` on the Content component: + +## Accessibility + +- Respects motion preferences: tooltip entry motion is enabled only when `prefers-reduced-motion: no-preference` diff --git a/packages/raystack/components/accordion/accordion.module.css b/packages/raystack/components/accordion/accordion.module.css index 4925f2a84..6c85bd0bd 100644 --- a/packages/raystack/components/accordion/accordion.module.css +++ b/packages/raystack/components/accordion/accordion.module.css @@ -49,7 +49,6 @@ color: var(--rs-color-foreground-base-secondary); pointer-events: none; flex-shrink: 0; - transition: transform 150ms ease-in-out; width: var(--rs-space-4); height: var(--rs-space-4); } @@ -57,7 +56,6 @@ .accordion-content { height: var(--accordion-panel-height); overflow: hidden; - transition: height 150ms ease-out; } .accordion-content[data-starting-style], @@ -78,4 +76,14 @@ align-self: stretch; padding: var(--rs-space-5) var(--rs-space-4); border-top: 0px; -} \ No newline at end of file +} + +@media (prefers-reduced-motion: no-preference) { + .accordion-icon { + transition: transform 150ms ease-in-out; + } + + .accordion-content { + transition: height 150ms ease-out; + } +} diff --git a/packages/raystack/components/button/button.module.css b/packages/raystack/components/button/button.module.css index 8ffd2bbe6..e175da8c3 100644 --- a/packages/raystack/components/button/button.module.css +++ b/packages/raystack/components/button/button.module.css @@ -13,7 +13,6 @@ padding: var(--rs-space-3) var(--rs-space-4); border-radius: var(--rs-radius-2); text-wrap: nowrap; - transition: all 0.2s ease-in-out; } .button:focus-visible { @@ -246,7 +245,16 @@ /* Todo: var does not exist for 10px */ width: 10px; height: 10px; - animation: spin 1s linear infinite; +} + +@media (prefers-reduced-motion: no-preference) { + .button { + transition: all 0.2s ease-in-out; + } + + .loader { + animation: spin 1s linear infinite; + } } .loader-text { diff --git a/packages/raystack/components/chip/chip.module.css b/packages/raystack/components/chip/chip.module.css index dcb0e501b..ff131ee85 100644 --- a/packages/raystack/components/chip/chip.module.css +++ b/packages/raystack/components/chip/chip.module.css @@ -11,7 +11,7 @@ border-radius: var(--rs-radius-2); white-space: nowrap; cursor: default; - transition: all 0.2s ease; + transition: var(--rs-transition-interactive); box-sizing: border-box; height: fit-content; width: fit-content; diff --git a/packages/raystack/components/collapsible/collapsible.module.css b/packages/raystack/components/collapsible/collapsible.module.css index d9791b672..7c94e3b1d 100644 --- a/packages/raystack/components/collapsible/collapsible.module.css +++ b/packages/raystack/components/collapsible/collapsible.module.css @@ -11,10 +11,15 @@ .panel { height: var(--collapsible-panel-height); overflow: hidden; - transition: height 150ms ease-out; } .panel[data-starting-style], .panel[data-ending-style] { height: 0; } + +@media (prefers-reduced-motion: no-preference) { + .panel { + transition: height 150ms ease-out; + } +} diff --git a/packages/raystack/components/dialog/dialog.module.css b/packages/raystack/components/dialog/dialog.module.css index 9660ed4f7..be7be6770 100644 --- a/packages/raystack/components/dialog/dialog.module.css +++ b/packages/raystack/components/dialog/dialog.module.css @@ -23,7 +23,7 @@ position: fixed; top: 50%; left: 50%; - transition: all 150ms; + transition: opacity 150ms; transform: translate(-50%, -50%); } @@ -84,11 +84,9 @@ z-index: var(--rs-z-index-portal); } -@media (prefers-reduced-motion: reduce) { - .dialogOverlay, +@media (prefers-reduced-motion: no-preference) { .dialogContent { - animation: none; - transition: none; + transition: opacity 150ms, transform 150ms; } } diff --git a/packages/raystack/components/drawer/drawer.module.css b/packages/raystack/components/drawer/drawer.module.css index 419970cfa..e647143f9 100644 --- a/packages/raystack/components/drawer/drawer.module.css +++ b/packages/raystack/components/drawer/drawer.module.css @@ -73,7 +73,6 @@ .drawerPopup-right { width: 250px; height: 100%; - transition: transform 450ms cubic-bezier(0.32, 0.72, 0, 1); transform: translateX(var(--drawer-swipe-movement-x)); } @@ -90,7 +89,6 @@ .drawerPopup-left { width: 250px; height: 100%; - transition: transform 450ms cubic-bezier(0.32, 0.72, 0, 1); transform: translateX(var(--drawer-swipe-movement-x)); } @@ -107,7 +105,6 @@ .drawerPopup-top { width: 100%; height: 300px; - transition: transform 450ms cubic-bezier(0.32, 0.72, 0, 1); transform: translateY(var(--drawer-swipe-movement-y)); } @@ -124,7 +121,6 @@ .drawerPopup-bottom { width: 100%; height: 300px; - transition: transform 450ms cubic-bezier(0.32, 0.72, 0, 1); transform: translateY(var(--drawer-swipe-movement-y)); } @@ -201,9 +197,11 @@ border-top: 1px solid var(--rs-color-border-base-primary); } -@media (prefers-reduced-motion: reduce) { - .drawerPopup, - .backdrop { - transition: none; +@media (prefers-reduced-motion: no-preference) { + .drawerPopup-right, + .drawerPopup-left, + .drawerPopup-top, + .drawerPopup-bottom { + transition: transform 450ms cubic-bezier(0.32, 0.72, 0, 1); } } diff --git a/packages/raystack/components/icon-button/icon-button.module.css b/packages/raystack/components/icon-button/icon-button.module.css index f4b5845f3..0fe473757 100644 --- a/packages/raystack/components/icon-button/icon-button.module.css +++ b/packages/raystack/components/icon-button/icon-button.module.css @@ -8,7 +8,7 @@ border: none; color: var(--rs-color-foreground-base-primary); cursor: pointer; - transition: all 0.2s ease; + transition: var(--rs-transition-interactive); padding: var(--rs-space-1); } diff --git a/packages/raystack/components/input-field/input-field.module.css b/packages/raystack/components/input-field/input-field.module.css index 87cf216b5..253520a48 100644 --- a/packages/raystack/components/input-field/input-field.module.css +++ b/packages/raystack/components/input-field/input-field.module.css @@ -29,7 +29,7 @@ border-radius: var(--rs-radius-2); border: 0.5px solid var(--rs-color-border-base-tertiary); background: var(--rs-color-background-base-primary); - transition: all 0.2s ease; + transition: var(--rs-transition-interactive); box-sizing: border-box; overflow: hidden; } @@ -155,7 +155,7 @@ .has-chips { background: var(--rs-color-background-base-primary); - transition: all 0.2s ease; + transition: var(--rs-transition-interactive); } .has-chips:hover { diff --git a/packages/raystack/components/meter/meter.module.css b/packages/raystack/components/meter/meter.module.css index 9d105c5be..15d9be392 100644 --- a/packages/raystack/components/meter/meter.module.css +++ b/packages/raystack/components/meter/meter.module.css @@ -42,7 +42,6 @@ .indicator { height: 100%; background-color: var(--rs-color-background-accent-emphasis); - transition: width 500ms; } /* Circular variant — viewBox is 72×72, SVG scales to container size */ @@ -83,7 +82,16 @@ (1 - var(--rs-meter-percentage, 0) / 100) ); stroke-linecap: butt; - transition: stroke-dashoffset 500ms; +} + +@media (prefers-reduced-motion: no-preference) { + .indicator { + transition: width 500ms; + } + + .circularIndicatorCircle { + transition: stroke-dashoffset 500ms; + } } .meter-variant-circular .value { diff --git a/packages/raystack/components/navbar/navbar.module.css b/packages/raystack/components/navbar/navbar.module.css index bd9ecc739..6c513efad 100644 --- a/packages/raystack/components/navbar/navbar.module.css +++ b/packages/raystack/components/navbar/navbar.module.css @@ -7,7 +7,12 @@ box-shadow: var(--rs-shadow-feather); box-sizing: border-box; min-height: var(--rs-space-11); - transition: transform 0.2s ease-in-out; +} + +@media (prefers-reduced-motion: no-preference) { + .root { + transition: transform 0.2s ease-in-out; + } } .root[data-sticky="true"] { diff --git a/packages/raystack/components/popover/popover.module.css b/packages/raystack/components/popover/popover.module.css index f8f82a330..85880bd4a 100644 --- a/packages/raystack/components/popover/popover.module.css +++ b/packages/raystack/components/popover/popover.module.css @@ -16,7 +16,6 @@ box-shadow: var(--rs-shadow-soft); border: 1px solid var(--rs-color-border-base-primary); color: var(--rs-color-foreground-base-primary); - animation: slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1); } @keyframes slideUpAndFade { @@ -29,3 +28,9 @@ transform: translateY(0); } } + +@media (prefers-reduced-motion: no-preference) { + .popover { + animation: slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1); + } +} diff --git a/packages/raystack/components/preview-card/preview-card.module.css b/packages/raystack/components/preview-card/preview-card.module.css index 0ae32d8cf..9dd1760d7 100644 --- a/packages/raystack/components/preview-card/preview-card.module.css +++ b/packages/raystack/components/preview-card/preview-card.module.css @@ -6,9 +6,7 @@ height: var(--positioner-height); width: var(--positioner-width); max-width: var(--available-width); - transition-property: top, left, right, bottom, transform; - transition-timing-function: var(--easing); - transition-duration: var(--animation-duration); + transition: opacity var(--animation-duration) var(--easing); } .popup { @@ -25,9 +23,7 @@ width: var(--popup-width, auto); height: var(--popup-height, auto); - transition-property: width, height, opacity, transform; - transition-timing-function: var(--easing); - transition-duration: var(--animation-duration); + transition: opacity var(--animation-duration) var(--easing); } .popup[data-starting-style], @@ -79,9 +75,7 @@ width: var(--popup-width); translate: 0; opacity: 1; - transition: - translate var(--animation-duration) var(--easing), - opacity calc(var(--animation-duration) / 2) var(--easing); + transition: opacity calc(var(--animation-duration) / 2) var(--easing); } .viewport[data-activation-direction~="left"] @@ -107,3 +101,30 @@ translate: -30% 0; opacity: 0; } + +@media (prefers-reduced-motion: no-preference) { + .positioner { + transition: + top var(--animation-duration) var(--easing), + left var(--animation-duration) var(--easing), + right var(--animation-duration) var(--easing), + bottom var(--animation-duration) var(--easing), + transform var(--animation-duration) var(--easing), + opacity var(--animation-duration) var(--easing); + } + + .popup { + transition: + width var(--animation-duration) var(--easing), + height var(--animation-duration) var(--easing), + opacity var(--animation-duration) var(--easing), + transform var(--animation-duration) var(--easing); + } + + .viewport [data-previous], + .viewport [data-current] { + transition: + translate var(--animation-duration) var(--easing), + opacity calc(var(--animation-duration) / 2) var(--easing); + } +} diff --git a/packages/raystack/components/progress/progress.module.css b/packages/raystack/components/progress/progress.module.css index dbd92e823..622a2f03a 100644 --- a/packages/raystack/components/progress/progress.module.css +++ b/packages/raystack/components/progress/progress.module.css @@ -42,7 +42,6 @@ .indicator { height: 100%; background-color: var(--rs-color-background-accent-emphasis); - transition: width 500ms; } /* Circular variant — viewBox is 72×72, SVG scales to container size */ @@ -83,7 +82,16 @@ (1 - var(--rs-progress-percentage, 0) / 100) ); stroke-linecap: butt; - transition: stroke-dashoffset 500ms; +} + +@media (prefers-reduced-motion: no-preference) { + .indicator { + transition: width 500ms; + } + + .circularIndicatorCircle { + transition: stroke-dashoffset 500ms; + } } .progress-variant-circular .value { diff --git a/packages/raystack/components/scroll-area/scroll-area.module.css b/packages/raystack/components/scroll-area/scroll-area.module.css index be0c4ac66..713a42f0e 100644 --- a/packages/raystack/components/scroll-area/scroll-area.module.css +++ b/packages/raystack/components/scroll-area/scroll-area.module.css @@ -25,10 +25,7 @@ background: transparent; pointer-events: auto; position: relative; - transition: - width 150ms ease-out, - height 150ms ease-out, - opacity 150ms ease-out; + transition: opacity 150ms ease-out; } .scrollbar[data-orientation="vertical"] { @@ -58,6 +55,15 @@ touch-action: none; } +@media (prefers-reduced-motion: no-preference) { + .scrollbar { + transition: + width 150ms ease-out, + height 150ms ease-out, + opacity 150ms ease-out; + } +} + .corner { background: transparent; } diff --git a/packages/raystack/components/select/select.module.css b/packages/raystack/components/select/select.module.css index 519a71fb1..b1ff2862a 100644 --- a/packages/raystack/components/select/select.module.css +++ b/packages/raystack/components/select/select.module.css @@ -13,7 +13,7 @@ box-shadow: var(--rs-shadow-soft); border: 0.5px solid var(--rs-color-border-base-primary); min-width: var(--anchor-width); - transition: all 0.2s ease; + transition: var(--rs-transition-interactive); max-height: 320px; position: relative; overflow: auto; diff --git a/packages/raystack/components/sidebar/sidebar.module.css b/packages/raystack/components/sidebar/sidebar.module.css index 4a34eaf93..63aa7b4bd 100644 --- a/packages/raystack/components/sidebar/sidebar.module.css +++ b/packages/raystack/components/sidebar/sidebar.module.css @@ -8,7 +8,6 @@ align-items: flex-start; flex-shrink: 0; background: var(--rs-color-background-base-primary); - transition: width 0.2s ease; position: relative; } @@ -69,7 +68,7 @@ color: var(--rs-color-foreground-base-secondary); cursor: pointer; text-decoration: none; - transition: all 0.2s ease; + transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; box-sizing: border-box; } @@ -203,12 +202,8 @@ /* Keep in flow (no display: none) so header row height is preserved */ } -@media (prefers-reduced-motion: reduce) { - - .root, - .nav-item, - .nav-text, - .resizeHandle { - transition: none; +@media (prefers-reduced-motion: no-preference) { + .root { + transition: width 0.2s ease; } } \ No newline at end of file diff --git a/packages/raystack/components/skeleton/skeleton.module.css b/packages/raystack/components/skeleton/skeleton.module.css index 4abef1250..f495b4e32 100644 --- a/packages/raystack/components/skeleton/skeleton.module.css +++ b/packages/raystack/components/skeleton/skeleton.module.css @@ -24,7 +24,6 @@ var(--skeleton-highlight-color), transparent ); - animation: shimmer var(--skeleton-duration) infinite; } @keyframes shimmer { @@ -36,3 +35,9 @@ } } +@media (prefers-reduced-motion: no-preference) { + .animate::after { + animation: shimmer var(--skeleton-duration) infinite; + } +} + diff --git a/packages/raystack/components/spinner/spinner.module.css b/packages/raystack/components/spinner/spinner.module.css index b996568fa..c3e9f7d16 100644 --- a/packages/raystack/components/spinner/spinner.module.css +++ b/packages/raystack/components/spinner/spinner.module.css @@ -11,7 +11,6 @@ height: 30%; background-color: currentColor; border-radius: var(--rs-radius-full); - animation: spin 1.2s linear infinite; transform-origin: center 150%; } @@ -90,6 +89,12 @@ } } +@media (prefers-reduced-motion: no-preference) { + .pole { + animation: spin 1.2s linear infinite; + } +} + .spinner-size-1 { width: var(--rs-space-4); height: var(--rs-space-4); diff --git a/packages/raystack/components/switch/switch.module.css b/packages/raystack/components/switch/switch.module.css index 541b1a623..d6ef022ef 100644 --- a/packages/raystack/components/switch/switch.module.css +++ b/packages/raystack/components/switch/switch.module.css @@ -49,10 +49,15 @@ background: var(--rs-color-foreground-base-emphasis); border-radius: var(--rs-radius-full); transform: translateX(var(--rs-space-1)); - transition: transform 200ms ease-in-out; will-change: transform; } +@media (prefers-reduced-motion: no-preference) { + .thumb { + transition: transform 200ms ease-in-out; + } +} + /* Small switch thumb sizing */ .switch.small .thumb { width: var(--rs-space-4); diff --git a/packages/raystack/components/tabs/tabs.module.css b/packages/raystack/components/tabs/tabs.module.css index bf09b182b..b8d447a1f 100644 --- a/packages/raystack/components/tabs/tabs.module.css +++ b/packages/raystack/components/tabs/tabs.module.css @@ -94,11 +94,6 @@ background-color: var(--rs-color-background-base-primary); border-radius: var(--rs-radius-2); box-shadow: var(--rs-shadow-feather); - transition: - top 0.25s cubic-bezier(0.4, 0, 0.2, 1), - left 0.25s cubic-bezier(0.4, 0, 0.2, 1), - width 0.25s cubic-bezier(0.4, 0, 0.2, 1), - height 0.25s cubic-bezier(0.4, 0, 0.2, 1); top: var(--active-tab-top, 0); left: var(--active-tab-left, 0); width: var(--active-tab-width, 0); @@ -106,12 +101,13 @@ z-index: 0; } -/* Media query that matches when the user has -asked the OS/browser for less or no motion -(e.g. “Reduce motion” in accessibility settings). */ -@media (prefers-reduced-motion: reduce) { +@media (prefers-reduced-motion: no-preference) { .indicator { - transition: none; + transition: + top 0.25s cubic-bezier(0.4, 0, 0.2, 1), + left 0.25s cubic-bezier(0.4, 0, 0.2, 1), + width 0.25s cubic-bezier(0.4, 0, 0.2, 1), + height 0.25s cubic-bezier(0.4, 0, 0.2, 1); } } diff --git a/packages/raystack/components/text-area/text-area.module.css b/packages/raystack/components/text-area/text-area.module.css index 9c162948b..97c4b3b6e 100644 --- a/packages/raystack/components/text-area/text-area.module.css +++ b/packages/raystack/components/text-area/text-area.module.css @@ -66,11 +66,16 @@ line-height: var(--rs-line-height-small); color: var(--rs-color-foreground-base-primary); padding: var(--rs-space-3); - transition: all 0.2s ease; overflow: hidden; } -.textarea:hover:not(:disabled) { +@media (prefers-reduced-motion: no-preference) { + .textarea { + transition: all 0.2s ease; + } +} + +.textarea:hover { border-color: var(--rs-color-border-base-focus); background: var(--rs-color-background-base-primary-hover); } diff --git a/packages/raystack/components/toast/toast.module.css b/packages/raystack/components/toast/toast.module.css index 81d9e4f7e..e27f9b8a8 100644 --- a/packages/raystack/components/toast/toast.module.css +++ b/packages/raystack/components/toast/toast.module.css @@ -66,10 +66,7 @@ cursor: default; z-index: calc(1000 - var(--toast-index)); - transition: - transform 500ms cubic-bezier(0.22, 1, 0.36, 1), - opacity 500ms, - height 150ms; + transition: opacity 500ms; } /* ===== Vertical position: bottom ===== */ @@ -283,14 +280,11 @@ opacity: 0.85; } -/* ===== Reduced motion ===== */ -@media (prefers-reduced-motion: reduce) { +@media (prefers-reduced-motion: no-preference) { .root { - transition: none; - } - - .root[data-starting-style], - .root[data-ending-style] { - transform: none; + transition: + transform 500ms cubic-bezier(0.22, 1, 0.36, 1), + opacity 500ms, + height 150ms; } } diff --git a/packages/raystack/components/tooltip/tooltip.module.css b/packages/raystack/components/tooltip/tooltip.module.css index 98ec2e079..13204ac90 100644 --- a/packages/raystack/components/tooltip/tooltip.module.css +++ b/packages/raystack/components/tooltip/tooltip.module.css @@ -21,40 +21,9 @@ letter-spacing: var(--rs-letter-spacing-mini); width: fit-content; max-width: 400px; - animation-duration: 400ms; transform-origin: var(--transform-origin); } -.content[data-open][data-side="top"]:not([data-instant]) { - animation-name: slideDownAndFade; -} - -.content[data-open][data-side="right"]:not([data-instant]) { - animation-name: slideLeftAndFade; -} - -.content[data-open][data-side="bottom"]:not([data-instant]) { - animation-name: slideUpAndFade; -} - -.content[data-open][data-side="left"]:not([data-instant]) { - animation-name: slideRightAndFade; -} -.content[data-side="top"][data-align="start"]:not([data-instant]) { - animation-name: slideDownRightAndFade; -} - -.content[data-side="top"][data-align="end"]:not([data-instant]) { - animation-name: slideDownLeftAndFade; -} - -.content[data-side="bottom"][data-align="start"]:not([data-instant]) { - animation-name: slideUpRightAndFade; -} - -.content[data-side="bottom"][data-align="end"]:not([data-instant]) { - animation-name: slideUpLeftAndFade; -} .arrow svg { color: var(--rs-color-background-base-primary); @@ -158,3 +127,41 @@ transform: translate(var(--rs-space-1), var(--rs-space-1)); } } + +@media (prefers-reduced-motion: no-preference) { + .content { + animation-duration: 400ms; + } + + .content[data-open][data-side="top"]:not([data-instant]) { + animation-name: slideDownAndFade; + } + + .content[data-open][data-side="right"]:not([data-instant]) { + animation-name: slideLeftAndFade; + } + + .content[data-open][data-side="bottom"]:not([data-instant]) { + animation-name: slideUpAndFade; + } + + .content[data-open][data-side="left"]:not([data-instant]) { + animation-name: slideRightAndFade; + } + + .content[data-side="top"][data-align="start"]:not([data-instant]) { + animation-name: slideDownRightAndFade; + } + + .content[data-side="top"][data-align="end"]:not([data-instant]) { + animation-name: slideDownLeftAndFade; + } + + .content[data-side="bottom"][data-align="start"]:not([data-instant]) { + animation-name: slideUpRightAndFade; + } + + .content[data-side="bottom"][data-align="end"]:not([data-instant]) { + animation-name: slideUpLeftAndFade; + } +} diff --git a/packages/raystack/styles/effects.css b/packages/raystack/styles/effects.css index 74a6980bc..e660054b4 100644 --- a/packages/raystack/styles/effects.css +++ b/packages/raystack/styles/effects.css @@ -7,6 +7,9 @@ --rs-shadow-floating: 0px 1px 1px 0px rgba(0, 0, 0, 0.04), 0px 2px 8px 0px rgba(0, 0, 0, 0.04), 0px 3px 17px 0px rgba(0, 0, 0, 0.04), 0px 4px 30px 0px rgba(0, 0, 0, 0.13); /* xl */ --rs-shadow-inset: 0px 1px 1px 0px rgba(0, 0, 0, 0.04) inset; + /* Transitions */ + --rs-transition-interactive: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease; + /* Blurs */ /* Example usage: backdrop-filter:: var(--rs-blur-sm); */ --rs-blur-sm: blur(0.5px);