diff --git a/ts/components/AnimatedEmojiGalore.tsx b/ts/components/AnimatedEmojiGalore.tsx
index 1d9a3620f7..dbb4e49fdf 100644
--- a/ts/components/AnimatedEmojiGalore.tsx
+++ b/ts/components/AnimatedEmojiGalore.tsx
@@ -5,6 +5,7 @@ import React from 'react';
import { animated, to as interpolate, useSprings } from '@react-spring/web';
import { random } from 'lodash';
import { Emojify } from './conversation/Emojify';
+import { useReducedMotion } from '../hooks/useReducedMotion';
export type PropsType = {
emoji: string;
@@ -38,9 +39,11 @@ export function AnimatedEmojiGalore({
emoji,
onAnimationEnd,
}: PropsType): JSX.Element {
+ const reducedMotion = useReducedMotion();
const [springs] = useSprings(NUM_EMOJIS, i => ({
...to(i, onAnimationEnd),
from: from(i),
+ immediate: reducedMotion,
config: {
mass: 20,
tension: 120,
diff --git a/ts/components/App.tsx b/ts/components/App.tsx
index 9ab276a1e6..54053cebbd 100644
--- a/ts/components/App.tsx
+++ b/ts/components/App.tsx
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect } from 'react';
-import { Globals } from '@react-spring/web';
import classNames from 'classnames';
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
@@ -13,7 +12,6 @@ import { type AppStateType, AppViewType } from '../state/ducks/app';
import { SmartInstallScreen } from '../state/smart/InstallScreen';
import { StandaloneRegistration } from './StandaloneRegistration';
import { usePageVisibility } from '../hooks/usePageVisibility';
-import { useReducedMotion } from '../hooks/useReducedMotion';
type PropsType = {
state: AppStateType;
@@ -124,14 +122,6 @@ export function App({
document.body.classList.toggle('page-is-visible', isPageVisible);
}, [isPageVisible]);
- // A11y settings for react-spring
- const prefersReducedMotion = useReducedMotion();
- useEffect(() => {
- Globals.assign({
- skipAnimation: prefersReducedMotion,
- });
- }, [prefersReducedMotion]);
-
return (
;
@@ -124,7 +125,9 @@ export function AnimatedEmoji({
}: AnimatedEmojiProps): JSX.Element {
const height = EMOJI_HEIGHT * toScale;
+ const reducedMotion = useReducedMotion();
const { rotate, x, y } = useSpring({
+ immediate: reducedMotion,
from: {
rotate: fromRotate,
x: fromX,
@@ -142,6 +145,7 @@ export function AnimatedEmoji({
// These styles animate faster than Y.
// Reactions toasts animate with opacity so harmonize with that.
const { scale } = useSpring({
+ immediate: reducedMotion,
from: {
scale: 0.5,
},
diff --git a/ts/components/CallingRaisedHandsList.tsx b/ts/components/CallingRaisedHandsList.tsx
index aa77e281cf..e4bf260fc6 100644
--- a/ts/components/CallingRaisedHandsList.tsx
+++ b/ts/components/CallingRaisedHandsList.tsx
@@ -14,6 +14,7 @@ import { ModalHost } from './ModalHost';
import { drop } from '../util/drop';
import * as log from '../logging/log';
import { usePrevious } from '../hooks/usePrevious';
+import { useReducedMotion } from '../hooks/useReducedMotion';
export type PropsType = {
readonly i18n: LocalizerType;
@@ -174,6 +175,8 @@ export function CallingRaisedHandsListButton({
}: CallingRaisedHandsListButtonPropsType): JSX.Element | null {
const [isVisible, setIsVisible] = React.useState(raisedHandsCount > 0);
+ const reducedMotion = useReducedMotion();
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
const [opacitySpringProps, opacitySpringApi] = useSpring(
{
@@ -186,6 +189,7 @@ export function CallingRaisedHandsListButton({
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
const [scaleSpringProps, scaleSpringApi] = useSpring(
{
+ immediate: reducedMotion,
from: { scale: 0.9 },
to: { scale: 1 },
config: BUTTON_SCALE_SPRING_CONFIG,
diff --git a/ts/components/CallingToast.tsx b/ts/components/CallingToast.tsx
index 6de2e6f428..f4a086c35b 100644
--- a/ts/components/CallingToast.tsx
+++ b/ts/components/CallingToast.tsx
@@ -18,6 +18,7 @@ import { useIsMounted } from '../hooks/useIsMounted';
import type { LocalizerType } from '../types/I18N';
import { usePrevious } from '../hooks/usePrevious';
import { difference } from '../util/setUtil';
+import { useReducedMotion } from '../hooks/useReducedMotion';
const DEFAULT_LIFETIME = 5000;
const DEFAULT_TRANSITION_FROM = {
@@ -225,7 +226,10 @@ export function CallingToastProvider({
const toastsRemoved = difference(prevToasts, curToasts);
const toastsAdded = difference(curToasts, prevToasts);
+ const reducedMotion = useReducedMotion();
+
const transitions = useTransition(toasts, {
+ immediate: reducedMotion,
from: item => {
const enteringItemIndex = toasts.findIndex(
toast => toast.key === item.key
diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx
index 84e528aa26..71ff71dc8d 100644
--- a/ts/components/Lightbox.tsx
+++ b/ts/components/Lightbox.tsx
@@ -30,6 +30,7 @@ import { drop } from '../util/drop';
import { isCmdOrCtrl } from '../hooks/useKeyboardShortcuts';
import type { ForwardMessagesPayload } from '../state/ducks/globalModals';
import { ForwardMessagesModalType } from './ForwardMessagesModal';
+import { useReducedMotion } from '../hooks/useReducedMotion';
export type PropsType = {
children?: ReactNode;
@@ -322,9 +323,12 @@ export function Lightbox({
const thumbnailsMarginInlineStart =
0 - (selectedIndex * THUMBNAIL_FULL_WIDTH + THUMBNAIL_WIDTH / 2);
+ const reducedMotion = useReducedMotion();
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
const [thumbnailsStyle, thumbnailsAnimation] = useSpring(
{
+ immediate: reducedMotion,
config: THUMBNAIL_SPRING_CONFIG,
to: {
marginInlineStart: thumbnailsMarginInlineStart,
diff --git a/ts/components/PlaybackButton.tsx b/ts/components/PlaybackButton.tsx
index 89b9545a46..5884b0a698 100644
--- a/ts/components/PlaybackButton.tsx
+++ b/ts/components/PlaybackButton.tsx
@@ -4,6 +4,7 @@
import { animated, useSpring } from '@react-spring/web';
import classNames from 'classnames';
import React, { useCallback } from 'react';
+import { useReducedMotion } from '../hooks/useReducedMotion';
const SPRING_CONFIG = {
mass: 0.5,
@@ -27,9 +28,11 @@ export type ButtonProps = {
export const PlaybackButton = React.forwardRef(
function ButtonInner(props, ref) {
const { mod, label, variant, onClick, context, visible = true } = props;
+ const reducedMotion = useReducedMotion();
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
const [animProps] = useSpring(
{
+ immediate: reducedMotion,
config: SPRING_CONFIG,
to: { scale: visible ? 1 : 0 },
},
diff --git a/ts/components/PlaybackRateButton.tsx b/ts/components/PlaybackRateButton.tsx
index bb121bde7c..9a25a0bb62 100644
--- a/ts/components/PlaybackRateButton.tsx
+++ b/ts/components/PlaybackRateButton.tsx
@@ -5,6 +5,7 @@ import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { animated, useSpring } from '@react-spring/web';
import type { LocalizerType } from '../types/Util';
+import { useReducedMotion } from '../hooks/useReducedMotion';
const SPRING_CONFIG = {
mass: 0.5,
@@ -30,10 +31,12 @@ export function PlaybackRateButton({
onClick,
}: Props): JSX.Element {
const [isDown, setIsDown] = useState(false);
+ const reducedMotion = useReducedMotion();
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
const [animProps] = useSpring(
{
+ immediate: reducedMotion,
config: SPRING_CONFIG,
to: isDown ? { scale: 1.3 } : { scale: visible ? 1 : 0 },
},
diff --git a/ts/components/ProfileEditor.tsx b/ts/components/ProfileEditor.tsx
index f436968fe6..25ff7b3218 100644
--- a/ts/components/ProfileEditor.tsx
+++ b/ts/components/ProfileEditor.tsx
@@ -47,6 +47,7 @@ import { isWhitespace, trim } from '../util/whitespaceStringUtil';
import { UserText } from './UserText';
import { Tooltip, TooltipPlacement } from './Tooltip';
import { offsetDistanceModifier } from '../util/popperUtil';
+import { useReducedMotion } from '../hooks/useReducedMotion';
export enum EditState {
None = 'None',
@@ -824,8 +825,9 @@ function UsernameLinkTooltip({
children: React.ReactNode;
i18n: LocalizerType;
}) {
+ const reducedMotion = useReducedMotion();
const animatedStyles = useSpring({
- from: { opacity: 0, scale: 0.25 },
+ from: { opacity: 0, scale: reducedMotion ? 1 : 0.25 },
to: { opacity: 1, scale: 1 },
config: { mass: 1, tension: 280, friction: 25 },
delay: 200,
diff --git a/ts/components/StoryProgressSegment.tsx b/ts/components/StoryProgressSegment.tsx
index e3730ac198..246871bc57 100644
--- a/ts/components/StoryProgressSegment.tsx
+++ b/ts/components/StoryProgressSegment.tsx
@@ -29,10 +29,6 @@ export const StoryProgressSegment = memo(function StoryProgressSegment({
const [progressBarStyle] = useSpring(() => {
return {
- // Override default value from `Globals` to ignore "Reduce Motion" setting.
- // This animation is important for progressing through stories and is minor
- // enough that it shouldn't cause issues for users with sensitivity to motion.
- skipAnimation: false,
immediate: index !== currentIndex,
// Pause while we are waiting for a valid duration
pause: !playing || !isValidDuration(duration),
diff --git a/ts/components/conversation/TypingBubble.tsx b/ts/components/conversation/TypingBubble.tsx
index 4672f3fa0e..7844e782fd 100644
--- a/ts/components/conversation/TypingBubble.tsx
+++ b/ts/components/conversation/TypingBubble.tsx
@@ -13,6 +13,7 @@ import type { LocalizerType, ThemeType } from '../../types/Util';
import type { ConversationType } from '../../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../../state/selectors/badges';
import { drop } from '../../util/drop';
+import { useReducedMotion } from '../../hooks/useReducedMotion';
const MAX_AVATARS_COUNT = 3;
@@ -87,9 +88,11 @@ function TypingBubbleAvatar({
i18n: LocalizerType;
theme: ThemeType;
}): ReactElement | null {
+ const reducedMotion = useReducedMotion();
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
const [springProps, springApi] = useSpring(
{
+ immediate: reducedMotion,
config: SPRING_CONFIG,
from: shouldAnimate
? AVATAR_ANIMATION_PROPS[visible ? 'hidden' : 'visible']
diff --git a/ts/hooks/useAnimated.tsx b/ts/hooks/useAnimated.tsx
index 2f56550189..ce8dde1a72 100644
--- a/ts/hooks/useAnimated.tsx
+++ b/ts/hooks/useAnimated.tsx
@@ -4,6 +4,7 @@
import { useState, useCallback } from 'react';
import type { SpringValues } from '@react-spring/web';
import { useChain, useSpring, useSpringRef } from '@react-spring/web';
+import { useReducedMotion } from './useReducedMotion';
export type ModalConfigType = {
opacity: number;
@@ -33,6 +34,7 @@ export function useAnimated(
modalStyles: SpringValues;
overlayStyles: SpringValues;
} {
+ const reducedMotion = useReducedMotion();
const [state, setState] = useState(ModalState.Opening);
const shouldShowModal =
state === ModalState.Open || state === ModalState.Opening;
@@ -41,6 +43,7 @@ export function useAnimated(
const modalRef = useSpringRef();
const modalStyles = useSpring({
+ immediate: reducedMotion,
from: getFrom(shouldShowModal),
to: getTo(shouldShowModal),
onRest: () => {