Skip to main content

useAnimation

useAnimation is a hook that helps create accessible animations by honouring the user Reduce Motion preference.

note

This hook uses the react-native built-in Animated API, check here for accessible animations in Reanimated.

Usage

import { useAnimation } from 'react-native-ama';

const { animatedStyle, progress, play } = useAnimation({
duration,
useNativeDriver,
from: {},
to: {},
skipIfReduceMotionEnabled,
});
PropertyTypeDescription
durationnumberthe animation duration
useNativeDriverbooleanUsing the native driver
fromViewStylethe initial state of the animation
toViewStylethe final state of the animation
skipIfReduceMotionEnabledboolean(Optional) if true, the animation will be played with duration 0 when Reduce Motion is enabled.

Returns

PropertyTypeDescription
animatedStyleObjectthe animation style to apply at the component
progressAnimated.ValueThe Animated.Value used to trigger the animation
play(toValue = 1) => Animated.timingReturns the Animated.timing

Example

The following animations translate in, with fading, an absolute positioned view:

import React, { useRef } from 'react';
import { Animated, Dimensions, StyleSheet, View } from 'react-native';
import { Pressable, Text } from 'react-native-ama';
import { useAccessibleAnimation } from 'react-native-ama';

export const ReduceMotionScreen = () => {
const [overlayProgressValue, setOverlayProgressValue] =
React.useState<Animated.Value | null>(null);
const animationProgress = useRef<Animated.Value>(
new Animated.Value(0),
).current;
const { play, animatedStyle, progress } = useAccessibleAnimation({
duration: 300,
useNativeDriver: true,
from: {
opacity: 0,
transform: [{ translateY: 200 }],
},
to: {
opacity: 1,
transform: [{ translateY: 0 }],
},
});

const overlayStyle = {
opacity: overlayProgressValue || 0,
};

const playAnimation = () => {
setOverlayProgressValue(progress);
play().start();
};

return (
<>
<View style={styles.container}>
<Pressable title="Test Animation 1" onPress={playAnimation1} />
</View>
{overlayProgressValue ? (
<Pressable
accessibilityRole="button"
accessibilityLabel="Close popup"
style={StyleSheet.absoluteFill}
onPress={() => reverseAnimation()}>
<Animated.View style={[styles.overlay, overlayStyle]} />
</Pressable>
) : null}
<Animated.View style={[styles.animation1, animatedStyle]}>
<Text style={styles.text}>Content goes here</Text>
</Animated.View>
</>
);
};

This is the result when we play the animation with Reduce Motion off and on:

| Reduce Motion: off | Reduce Motion: on | | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | |

|
|
|

When reduce Motion is off, the animation is played as specified; while when is on any motion animation is played instantaneity (using a duration of 0), while other properties, like opacity, are played as specified.

Sequential animation

Let's consider the following animation:


The animation is defined as:

const {
play: play2,
animatedStyle: animatedStyle2,
progress: progress2,
} = useAccessibleAnimation({
duration: 300,
useNativeDriver: false,
from: {
opacity: 0,
width: 0,
left: MAX_LINE_WIDTH / 2,
},
to: {
opacity: 1,
width: MAX_LINE_WIDTH,
left: theme.padding.big,
},
});
const { play: play3, animatedStyle: animatedStyle3 } = useAccessibleAnimation({
duration: 300,
useNativeDriver: false,
from: {
height: 2,
marginTop: -1,
},
to: {
height: 200,
marginTop: -100,
},
});

const playAnimation = () => {
play2().start(() => {
play3().start();
});
};

It's a two-part animation, where the first one the animates the view width and opacity:

from: {
opacity: 0,
width: 0,
left: MAX_LINE_WIDTH / 2,
},
to: {
opacity: 1,
width: MAX_LINE_WIDTH,
left: theme.padding.big,
}

The second one the height:

from: {
height: 2,
marginTop: -1,
},
to: {
height: 200,
marginTop: -100,
},

Let's play the animation when reduce motion is enabled:


The animation doesn't look right. The first animation is played correctly, but:

  • the width animation is played with a duration of 0s
  • the fade animation is played with a duration of 300ms

After that, the height jumps instantly from 2 to 200.

skipIfReduceMotionEnabled

One way to fix the animation is using the skipIfReduceMotionEnabled parameter; as this makes all the animations defined to be played instantly:

const {
play: play2,
animatedStyle: animatedStyle2,
progress: progress2,
} = useAccessibleAnimation({
duration: 300,
useNativeDriver: false,
skipIfReduceMotionEnabled: true,
from: {
opacity: 0,
width: 0,
left: MAX_LINE_WIDTH / 2,
},
to: {
opacity: 1,
width: MAX_LINE_WIDTH,
left: theme.padding.big,
},
});

The result is: