Advanced animation in Angular: Part 2. Disable any animations by prefers-reduced-motion
Using prefers-reduced-motion
to manage effectively CSS animations
and Angular animations
like a Pro
Previous part
Introduction
Animations today are an integral part of modern web applications. Animations help us to improve the perception of our application to the users by giving them feedback about their actions, which helps to make the user experience pleasant and memorable.
But there are cases when we should not use animations:
- Users with restrictions. For them, animation is not an embellishment, but a distracting part that may be less accessible and interfere with the perception
- E2E tests. It takes time to wait for each animation and transition. The more tests there are, the more time is spent waiting for them to be completed
For these 2 cases, you can’t build a separate application without animations, you need to have an opportunity to disable animations.
And there is an easy way to do this.
Prefers-reduced-motion
The
prefers-reduced-motion
CSS
media feature is used to detect if a user has enabled a setting on their device to minimize the amount of non-essential motion. The setting is used to convey to the browser on the device that the user prefers an interface that removes, reduces, or replaces motion-based animations. ( You can find ways to activate it here )
Since this is an @media
setting, all code can be handled in the @media
block
@media (prefers-reduced-motion) {
transition: all 0 linear;
/* others styles */
}
Knowing about this customization, we just need to learn how to handle it properly in the Angular application.
Example application
First, I suggest implementing 2 elements with fade
animation in two ways:
- Angular animation
const leavedStyle = style({opacity: 0});
const enteredStyle = style({opacity: 1});
export const fade = trigger('fade', [
transition(
':enter',
[leavedStyle, animate('300ms linear', enteredStyle)],
),
transition(
':leave',
[enteredStyle, animate('300ms linear', leavedStyle)],
),
]);
<app-item @fade *ngIf="state" class="item">
Angular animation item
</app-item>
- CSS animation
.show {
opacity: 1;
}
.hide {
opacity: 0;
}
.css-animation {
transition: opacity 300ms linear;
}
<app-item class="css-animation item" [ngClass]="state ? 'show' : 'hide'">
Css animation item
</app-item>
And let’s also add a style indicator for each element. This would help us visually tell if the prefers-reduced-motion
setting is turned on
on the device
.item {
background: #ffa6a6; // red
@media (prefers-reduced-motion: reduce) {
background: #8eff8a; // green
}
}
Let’s run it and check it out
With prefers-reduced-motion
enabled, the display works, but so far this option does not affect the animation
CSS animation и prefers-reduced-motion
Let’s start with the easy part.
As it’s shown in the example above, style management with prefers-reduced-motion
is done via the standard @media
block.
We can combine styles and selectors with this block in various ways to get a solution. But in my opinion, the most elegant and simple solution is to control it via a global CSS variable
.
To do this, we need to add a CSS variable
with the animation time to styles.scss
, and also specify a new animation time for prefers-reduced-motion:reduce
:root {
--animation-duration: 300ms;
}
@media (prefers-reduced-motion: reduce) {
:root {
--animation-duration: 0ms;
}
}
Also, let’s change our past animation implementation, which now takes the CSS variable --animation-duration
into account
.css-animation {
- transition: opacity 300ms linear;
+ transition: opacity var(--animation-duration) linear;
}
Let’s check
As you can see from this example, with prefers-reduced-motion
enabled, we already affect CSS animation
with just one global parameter.
The only thing left to do is to add support for Angular animation
.
Angular animation и prefers-reduced-motion
In the previous part, I told about how you can make reusable Angular animations
customizable with parameters. And the current case is no exception.
Let’s make the animation customizable
const leavedStyle = style({ opacity: 0 });
const enteredStyle = style({ opacity: 1 });
const animateTimings = '{{duration}}ms linear';
const defaultOptions = {params: {duration: 300}};
export const fade = trigger('fade', [
transition(
':enter',
[leavedStyle, animate(animateTimings, enteredStyle)],
defaultOptions,
),
transition(
':leave',
[enteredStyle, animate(animateTimings, leavedStyle)],
defaultOptions,
),
]);
There are several options for how you can indicate to Angular whether the prefers-reduced-motion
setting is enabled, but the best solution, in my opinion, is to use the DI
mechanism, which will provide parameters for animations.
Besides the fact that the parameters will be global, it will also provide consistency for current and new Angular animations
. After all, all customized animations for transitions should now use duration
as the time parameter.
Let’s implement a token with parameters for Angular animation
function isReducedMotion(): boolean {
const windowRef = inject(DOCUMENT).defaultView;
const isReduce = windowRef?.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
return isReduce ?? false;
}
export type TAnimationParams = AnimationOptions['params'] & {
duration: number;
}
export const ANIMATION_PARAMS_TOKEN = new InjectionToken<TAnimationParams>(
'ANIMATION_PARAMS_TOKEN', {
factory: () => ({
duration: isReducedMotion() ? 0 : 300
}),
providedIn: 'root'
})
Implement a token with parameters and declare settings for animation with them in mind
constructor(
@Inject(ANIMATION_PARAMS_TOKEN)
private animationParams: TAnimationParams
) {}
public animationOptions: ReusableAnimationOptions<string> = {
value: '_', // or animation state name
params: {
// current params
...this.animationParams
}
}
Passing the animation settings in the template
- <app-item @fade *ngIf="state" class="item">
+ <app-item [@fade]="animationOptions" *ngIf="state" class="item">
Angular animation item
</app-item>
Let’s check the result
As you can see, prefers-reduced-motion
now affects Angular animation
, and it works similarly to CSS animation
when disabled.
If prefers-reduced-motion
is disabled on the device, the animations will work as normal
Great 🎉
Conclusion
prefers-reduced-motion
helps us create an app with disabled animations that is useful not only for us in e2e tests
, but also for users with restrictions
.
The examples shown here can be used to correctly react to other global user settings that should affect the app, preventing the creation of an alternative app for every case.
Q&A
- Could we not create
tokens DI
to provide parameters for theAngular animation
, but go straight towindow
to create the animation with the parameters already ready?
Yes, but then there would be quite a few errors:
- Accessing a global
window
object. Working with global functions is considered a bad practice in Angular, and if any Window API may be tokenized and handled viaDI
.
Especially if you are working with SSR where there is no browser environment. - Code overload. The point of this approach is that the animation itself doesn’t know if there is a
prefers-reduced-motion
setting and whether to respond to it. This is especially true when Angular animation is reused in multiple applications with noe2e-tests
orusers with restrictions
accounting. It is up to the application team to decide whether the animation should be disabled or not.
2. Why isn’t NoopAnimationsModule
used to turn off Angular animations
?
There are several reasons for this:
NoopAnimationsModule
is primarily used inunit
andintegration
tests, where the test environment can be created viaTestBed
. In the case ofe2e-tests
, the test is performed over the build of the application, not as a part of the application- Alternatively building the application with
NoopAnimationsModule
fore2e-tests
breaks the integrity of the test, because now the tests are performed on the “other” application NoopAnimationsModule
disables allAngular animations
, not specific ones at our request
3. Why couldn’t we use the withConfig
function call of BrowserAnimationsModule
to disable animations?
The current implementation helps us to control specific animations rather than disabling them altogether. If your application does not have any special animation requirements for this mode, you can use this method as well.