A variation of the accessibiliy icon

Better User Experience: Removing Animations for People Troubled by Motion (CSS / Angular)

Lesezeit
10 ​​min

When building websites we not only try to deliver a solid, tested and maintainable solution, we also always strive for a great user experience. For some users who are troubled by motion when looking at websites this means restricting or even removing animations. This article shows to create a fitting solution.

Users differ greatly in their needs and limitations, some are:

  • Some users cannot use a mouse (for example, permanently because they have muscular problems or temporarily when carrying a child on their arm). So we make our websites accessible by multiple input devices such as mouse, touchscreen and keyboard.
  • Some users are visually impaired (because they are color blind or their vision is decreased). So we use proper color palettes, high contrast and make the website scalable.
  • Some users are troubled by motion (because they may have a vestibular motion disorder or are easily distracted).

For the last group, animations in a website can be a burden. However, animations are a good thing, as they can help the user processing the contents of a website better. They can make a fine website a great one. Therefore, we do not want to remove animations in general but only for those users that prefer reduced motion.

And we can do exactly that with a media query called prefers-reduced-motion:

@media (prefers-reduced-motion: reduce) {}

The browser support is quite good since almost all modern browsers have it; see caniuse.com.

Disable animations in CSS

The first idea on how to disable animations in CSS is probably this:

@media (prefers-reduced-motion: reduce) {
  *, *::after, *::before {
    animation: none !important;
    transition: none !important;
  }
}

but this breaks JavaScript code that listens to transitionend or related events. Setting the animation-duration: 0s does not work either because in some browsers this may lead to no style changes at all (for a discussion, see https://github.com/jensimmons/cssremedy/issues/11).

The most robust way we found is this:

@media (prefers-reduced-motion: reduce) {
  *, *::after, *::before {
    animation-duration: 1ms !important;      /* 1 */
    animation-delay: -1ms !important;        /* 2 */
    animation-iteration-count: 1 !important; /* 3 */
    transition-duration: 1ms !important;     /* 4 */
    transition-delay: -1ms !important;       /* 5 */
  }  
}
  1. We set a very short animation-duration, thereby still firing animation events.
  2. We set a negative animation-delay to even out the duration. Some browsers ignore negative delays, so they get a very fast animation instead (hopefully not noticeable).
  3. To end animations after one iteration (prevent infinite repeats), we set the animation-iteration-count.
  4. To also remove transitions (but keep transition events) we set a very short transition-duration.
  5. We set a negative transition-delay to even out the transition duration.

All five declarations contain the !important rule to override the specificity of any selector. This, of course, only works as long as we do not use !important anywhere else in our declarations (which we should not).

This is a very radical approach, since we remove all animations and transitions everywhere on the page, but it does its job. A more sensible approach follows further below.

Disable animations in Angular

We should declare animations and transitions in CSS. However, not everything can be animated by using CSS only. For example, closing an element by animating its height to 0 cannot be done without JavaScript because in CSS we can only animate from one specific value to another. We cannot animate from intrinsic height to 0.

Angular offers a full module to help animate components where CSS alone is not sufficient. It is quite easy to solve the height animation problem with Angular. For a working example, see https://codesandbox.io/s/disable-animations-in-angular-1pctb?file=/src/app/app.module.ts.

To use the Angular animations module, we import it in our app.module.ts:

import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from
  "@angular/platform-browser/animations";
import { NgModule } from "@angular/core";
import { AppComponent } from "./app.component";

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

This is where we disable all Angular animations when a user prefers reduced motion. Instead of removing all animations in each component, we use a second animations module provided by Angular, the NoopAnimationsModule. The NoopAnimationsModule still provides the style changes but does not animate at all, while still keeping track of animation event listeners.

import { BrowserModule } from "@angular/platform-browser";
import { NoopAnimationsModule } from
  "@angular/platform-browser/animations";
import { NgModule } from "@angular/core";
import { AppComponent } from "./app.component";

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    NoopAnimationsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})

export class AppModule {}

Now we have disabled all Angular animations by using the NoopAnimationsModule. To provide animations depending on user preference, we have to load the one or the other animations module. In JavaScript, we have access to the media queries by using the matchMedia function:

const disableAnimations = window.matchMedia(
  "(prefers-reduced-motion: reduce)"
).matches;

The matchMedia function returns an object with (among others) a boolean property matches.

To put it together, we disable Angular animations with these lines:

import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from 
  "@angular/platform-browser/animations";
import { NoopAnimationsModule } from
  "@angular/platform-browser/animations";
import { NgModule } from "@angular/core";
import { AppComponent } from "./app.component";

// detect whether the user prefers reduced motion
const disableAnimations: boolean = window.matchMedia(
  "(prefers-reduced-motion: reduce)"
).matches;

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    
    // conditionally load the AnimationsModule
    disableAnimations 
      ? NoopAnimationsModule
      : BrowserAnimationsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

How to activate the reduced motion setting

In general, this is not a browser setting but a setting that you can activate on your device. The Mozilla Developer Network has a list of how to activate this setting on various operating systems.

Removing animations or reducing animations?

As said before, animations can improve the user experience. Removing them entirely may not be a good idea. And we do not have to. The media query is called prefers-reduced-motion, not prefers-no-motion nor does-absolutely-not-want-any-motion. For each animation we can ask ourselves (or even better: our users) whether it adds to the user experience or is just a fancy but unnecessary effect.

It is totally fine to keep some animations, especially when induced by a user action. When the user clicks a submit button but the form cannot be submitted because one field is not filled, it may be a good idea to draw attention to that field by, say, animating a box shadow. On the other hand, a pulsing icon trying to notify the user of a new feature that they might not be interested in seems to be an animation that could or should be removed.

Instead of the radical solution presented above, we should act on a case-by-case basis. When we want to keep some valuable animations while removing those that might disturb some users (see the example in action on codepen):

/* valuable transition */
input {
  transition: box-shadow 0.3s ease-in-out;
}
input:invalid {
  box-shadow: 0 0 0.2em red;
}

/* fancy animation, only for users without motion preference */
@media (prefers-reduced-motion: no-preference) {
  input {
    transition:
      box-shadow 0.3s ease-in-out,
      transform 1s cubic-bezier(.29, 1.01, 1, -0.68);
  }
  input:invalid {
    transform: rotate(720deg);
  }
}

What about older browsers?

Not all browsers support the prefers-reduced-motion media query. But that is fine. We can still use the media query, only those browsers will ignore it. We have two options:

  1. Disable animations generally and only add them on a case-by-case basis with @media (prefers-reduced-motion: no-preference). This way, users with older browsers do not get animations whether they want them or not.
  2. Disable animations only when explicitly set and supported with @media (prefers-reduced-motion: reduce). This way, users of older browsers may get animations even if they would rather not.

To choose an option for your website, you have to ask yourself: How much do users benefit from animations? To which level do they disturb some users?

And keep in mind: Old browsers often run on old computers. Disabling animations may improve performance and battery life.

Conclusion and outlook

When building a website we should focus on the users and their needs and limitations. For users with a vestibular motion disorder, removing unneeded animations is one aspect of that. But motion on a website is not limited to CSS transitions and animations. To further improve the User Experience think of other parts:

  • carousels
  • gif emojis
  • advertisement (you probably have no influence on that)
  • auto-playing videos
  • and more

 

Hat dir der Beitrag gefallen?

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Ähnliche Artikel