Add Support for Reduced Motion in Angular Animations

Post Editor

Animations are good, but it can be overwhelming sometimes. As developers, we need to allow users to take control of animations. In this article we will learn how we can utilize reduced motion media query to disable angular animations.

7 min read
0 comments
post

Add Support for Reduced Motion in Angular Animations

Animations are good, but it can be overwhelming sometimes. As developers, we need to allow users to take control of animations. In this article we will learn how we can utilize reduced motion media query to disable angular animations.

post
post
7 min read
0 comments
0 comments

In this article, we will understand why reduced motion support is needed. We will also understand the media query and it’s various usages. And at last, we will see how to disable animations in Angular.

If you’re simply interested in the main code, head over to Disable Animations. There we’re going to create a service, which will help us identify user’s preference for reduced motion. Or take a look at code directly.

Animation
Link to this section

Animations can be used in all kinds of web pages. Oftentimes they’re used to provide feedback to the user to indicate that an action is received and being processed. It can be a small animation that happens when a user scrolls or a bouncy animation to showcase a new product. We generally see a slide-from-top animation for banners, and announcements on most of the web pages.

But not everyone likes animations and there are folks with seasickness or vestibular motion disorder. Disabling or reducing animations is an important Accessibility feature. Fortunately, CSS media query `prefers-reduced-motion` helps developers to serve the users who fall in that category.

prefers-reduced-motion
Link to this section

The prefers-reduced-motion media query detects whether the user has requested the operating system to minimize the amount of animation or motion it uses.

It can take two values:

no-preference - Indicates that the user has made no preference known to the system. This keyword value evaluates as false in the boolean context.

reduce - Indicates that user has notified the system that they prefer an interface that minimizes the amount of movement or animation, preferably to the point where all non-essential movement is removed.

This media query is still in draft of Media Queries Level 5, but the majority of latest browsers support it today.

Usage with CSS
Link to this section

With CSS, you can simply use it like below:

<>Copy
/* If the user has expressed their preference for reduced motion, then don't use animations on loader. */ @media (prefers-reduced-motion: reduce) { .loader { animation: none; } } /* If the browser understands the media query and the user explicitly hasn't set a preference, then use animations on loaders. */ @media (prefers-reduced-motion: no-preference) { .loader { /* `spin` keyframes are defined elsewhere */ animation: spin 0.5s linear infinite both; } }

Another way is to have all of your animations in separate file and load it conditionally via `media` attribute with `link` element:

<>Copy
<link rel="stylesheet" href="animations.css" media="(prefers-reduced-motion: no-preference)">

Usage with JavaScript
Link to this section

Browsers will handle CSS rules dynamically when preference is changed. But with JavaScript, we will have to listen for changes and programmatically handle the animations.

<>Copy
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); mediaQuery.addEventListener('change', () => { console.log(mediaQuery.media, mediaQuery.matches); // Stop JavaScript-based animations. });

Let’s see how the above approach can help us with Angular animations.

Angular Animations
Link to this section

Angular's animation system is built on CSS functionality, which means you can animate any property that the browser considers animatable. If you’re working with Angular animation for the first time, I would recommend you to read the article by @williamjuan27: In-Depth guide into animations in Angular - Angular inDepth.

A Service to Disable Animations
Link to this section

We are going to create a service, which will have an observable of prefers-reduced-motion media query. And then, we can use it in components to disable the animations.

MediaMatchService

If you’re using Angular CLI, you can quickly create the service using below command:

<>Copy
ng g s core/services/media-match

Let’s modify the content of media-match.service.ts:

<>Copy
// src/app/core/services/media-match.service.ts import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; // types export interface MediaQueriesMap { _type: MediaQueryType; _query: string; } export type MediaQueryType = 'prefers-reduced-motion'; // media queries list export const MEDIA_QUERIES: MediaQueriesMap[] = [ { _type: 'prefers-reduced-motion', _query: '(prefers-reduced-motion: reduce)', }, ]; @Injectable({ providedIn: 'root' }) export class MediaMatchService { private _mediaQueryListeners!: { [key in MediaQueryType]: BehaviorSubject<boolean>; }; public mediaQueryListeners$!: { [key in MediaQueryType]: Observable<boolean>; }; constructor() { MEDIA_QUERIES.forEach((mq) => this.matchMedia(mq)); } private matchMedia(mq: MediaQueriesMap) { this._mediaQueryListeners = { ...this._mediaQueryListeners, [mq._type]: new BehaviorSubject<boolean>(false), }; this.mediaQueryListeners$ = { ...this.mediaQueryListeners$, [mq._type]: this._mediaQueryListeners[mq._type].asObservable(), }; const mediaQueryList = window.matchMedia(mq._query); this._mediaQueryListeners[mq._type].next(mediaQueryList.matches); mediaQueryList.addEventListener('change', (ev: MediaQueryListEvent) => { this._mediaQueryListeners[mq._type].next(ev.matches); }); } }

Here’s what’s going on with above code:

<>Copy
export const MEDIA_QUERIES: MediaQueriesMap[] = [ { _type: 'prefers-reduced-motion', _query: '(prefers-reduced-motion: reduce)', }, ];

First, we’re defining a constant, which holds an array of media query types and their actual queries.

<>Copy
private _mediaQueryListeners!: { [key in MediaQueryType]: BehaviorSubject<boolean>; };

Second, we’re creating a JSON object, which will contain a subject for each media query type. It will emit true or false based on MediaQueryList.matches.

<>Copy
public mediaQueryListeners$!: { [key in MediaQueryType]: Observable<boolean>; };

Third, we are creating JSON of observables, which will be based on subjects we created earlier. The consumers of this service will use this property.

<>Copy
constructor() { MEDIA_QUERIES.forEach((mq) => this.matchMedia(mq)); }

Fourth, we’re building previous two JSON properties by calling a method matchMedia for each set of MEDIA_QUERIES constant.

Now, let’s look at the method matchMedia:

<>Copy
private matchMedia(mq: MediaQueriesMap) { this._mediaQueryListeners = { ...this._mediaQueryListeners, [mq._type]: new BehaviorSubject<boolean>(false), }; this.mediaQueryListeners$ = { ...this.mediaQueryListeners$, [mq._type]: this._mediaQueryListeners[mq._type].asObservable(), }; }

This method takes one argument, mq: MediaQueriesMap, which is a set of media query type (`_type`) and actual query (`_query`). First it's updating the existing JSON of subjects by adding a subject for the new _type. And then using the same subject, it is updating JSON of observables for the same _type.

<>Copy
private matchMedia(mq: MediaQueriesMap) { // ... const mediaQueryList = window.matchMedia(mq._query); this._mediaQueryListeners[mq._type].next(mediaQueryList.matches); mediaQueryList.addEventListener('change', (ev: MediaQueryListEvent) => { this._mediaQueryListeners[mq._type].next(ev.matches); }); }

After updating the subjects and observables, it is time to get the query result. Above code is pretty clear and simple.

Note that we are emitting the result of mediaQueryList.matches even before listening to the change event. Reason behind that is to have the initial value emitted from the service. If we don’t do that and simply listen for changes, it might happen that users have already set reduced as their motion preference when they visit the app, but our app will still continue showing animations.

One advantage of keeping JSON of subjects/observables is that you can add many more media queries to it. For instance, you want to create a media query to check for landscape orientation, you would do like below:

  1. Add type in MediaQueryType, so it would become:
<>Copy
export type MediaQueryType = 'prefers-reduced-motion' | 'orientation-landscape';

2.  Add actual query in MEDIA_QUERIES:

<>Copy
export const MEDIA_QUERIES: MediaQueriesMap[] = [ { _type: 'prefers-reduced-motion', _query: '(prefers-reduced-motion: reduce)', }, { _type: 'orientation-landscape', _query: '(orientation:landscape)', }, ];

3.  And consume it in your component:

<>Copy
public orientationLandscape$ = this.mediaMatch.mediaQueryListeners$['orientation-landscape'];

App with animations
Link to this section

To enable animations in angular, we need to import BrowserAnimationsModule in the root module.

I have created an app, which has a basic UI, and setup with animations and services. You can checkout the code on GitHub Repo.

Let’s look at the output of app with animations:

Output of app with animation
Output of app with animation

App with disabled animations
Link to this section

To disable the animations, first we’re going to consume the service MediaMatchService in src/app/app.component.ts:

<>Copy
// src/app/app.component.ts // ... export class AppComponent implements OnInit, OnDestroy { //... disableAnimations$ = this.mediaMatch.mediaQueryListeners$['prefers-reduced-motion']; constructor( private mediaMatch: MediaMatchService ) {} // ... }

And then, a special animation control binding called @.disabled can be placed on an HTML element to disable animations on that element, as well as any nested elements. When true, the @.disabled binding prevents all animations from rendering.

Let’s put it in src/app/app.component.html:

<>Copy
<!-- src/app/app.component.html --> <div class="container" [@.disabled]="disableAnimations$ | async"> <!-- rest remains same --> </div>

Let’s see now how our app looks when rendered with reduced motion:

App with disabled animation in reduced motion
App with disabled animation in reduced motion

As you would see, now it doesn’t render the app with animations.

NoopAnimationsModule
Link to this section

We can also use NoopAnimationsModule to disable angular animations for particular modules. Simply import it instead of BrowserAnimationsModule.

But prefers-reduced-motion provides much more flexibility, because it allows to load animations based on user preferences very easily.

Controlling animations from UI
Link to this section

If you don’t want to use prefers-reduced-motion query, you could integrate some controls, like switch or checkbox, which will allow users to disable or enable animations runtime.

Netlify’s Netlify Reaches One Million Devs! Website is a nice example of this approach, they’ve added a switch on the top-left side to control animations.

A control switch to disable animations on Netlify Reaches One Million Devs!
A control switch to disable animations on Netlify Reaches One Million Devs!

But, disabling animations through media query is recommended over this, because it’s more accessible and saves users from performing extra actions.

Angular v12
Link to this section

Angular team is working on bringing a feature to add support for disabling animations through BrowserAnimationsModule.withConfig. This is already available in v12.0.0-next.3:

<>Copy
import { NgModule } from "@angular/core"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { AppComponent } from "./app.component"; export function prefersReducedMotion(): boolean { const mediaQueryList = window.matchMedia("(prefers-reduced-motion)"); return mediaQueryList.matches; } @NgModule({ imports: [ BrowserAnimationsModule.withConfig({ disableAnimations: prefersReducedMotion() }) ], declarations: [AppComponent], bootstrap: [AppComponent] }) export class AppModule {}

Conclusion
Link to this section

We saw the usages of prefers-reduced-motion media query, it’s importance and how it helped us to disable animations in angular. We also saw a couple of other approaches to disable them, but media query is recommended over them.

And in v12, you can simply use BrowserAnimationsModule.withConfig to disable animations.

I have created a GitHub repo for all of the code we did above.

Further Reading

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

I am a Front-end Developer. I like to work on Angular, React, Bootstrap, CSS, SCSS & Electron. I also love to contribute to Open-Source Projects and sometime write articles.

author_image

About the author

Dharmen Shah

I am a Front-end Developer. I like to work on Angular, React, Bootstrap, CSS, SCSS & Electron. I also love to contribute to Open-Source Projects and sometime write articles.

About the author

author_image

I am a Front-end Developer. I like to work on Angular, React, Bootstrap, CSS, SCSS & Electron. I also love to contribute to Open-Source Projects and sometime write articles.

Looking for a JS job?
Job logo
Sr. Frontend Developer - Angular

Luxoft

United States
Remote
Job logo
Angular Java Developer

Digital Links Inc

United States
Remote
$120k - $130k
More jobs
NxAngularCli
NxAngularCli
NxAngularCli

Featured articles