← All writing
Craft · · 8 min

The apology

Accessibility CSS

They apologized to me. That’s the part that got me, and it’s most of the reason any of this is getting written down.

Earlier this month I built a parallax hero. The client asked for it, the designer had drawn it, the background moved at half the speed of the foreground as you scrolled. I got it running smoothly and, not gonna lie, I was pretty pleased with myself.

Then I walked over to a coworker’s desk to show it off. About 10 seconds in they said “can you close that.” Not as a joke, not doing a bit. They had a headache the rest of the afternoon, went home early, and then apologized to me for it.

I had all of this in the taste column. Is parallax tacky, is it dated, is it worth the JavaScript, do we really have to. Those are real questions and I have opinions about every one of them. They are all the wrong question, because for some number of the people loading your page this has nothing to do with taste at all. It’s their inner ear.

Sea legs at a desk

The word is vestibular, and the short version is that your balance system takes its cues partly from what your eyes are doing. When a large area of the screen moves in a way that says you’re moving, while everything else in your body says you’re sitting perfectly still in a chair, some people get the response they’d get on a boat.

Dizziness, nausea, disorientation, migraine. And not for the length of the animation, which is what I’d assumed. For hours afterward. My coworker lost an afternoon to 10 seconds of a hero image, none of it was voluntary, and knowing the page was flat didn’t help.

The triggers are pretty consistent, and once somebody lists them out for you it gets hard to stop noticing them. Large areas moving at different speeds to each other, which is the literal definition of parallax, so what I’d built was the textbook example. Zooming and scaling, especially a full-bleed image that grows while you watch it. Movement along the z-axis, anything flying toward your face. Spinning. And scroll-jacking, where the page decides how fast you’re allowed to move through it, which is the one I object to on my own account despite having no vestibular condition whatsoever.

The figures I found put it at something like a third of adults over 40 having experienced some vestibular trouble. I’m sure that number is doing a lot of work and covers everything from one bad week of vertigo to a condition somebody lives with, and even discounted heavily it took this out of the edge case column for me. Call it a couple of people in any given meeting.

Don’t throw out the fade with the parallax

The instinct, once somebody has explained all that to you, is to go strip every animation out of everything, and that overcorrects.

A lot of motion is carrying information. A panel that slides in from the right has told you where it came from and where it will go back to. A list item that fades as it’s removed has told you that a thing was removed, instead of that the page reloaded underneath you. Take all of that away and the interface isn’t safer, it’s harder to follow, and it’s hardest to follow for the people who were leaning on those cues most.

The line that works for me is between motion that carries something a distance across the screen and motion that changes an appearance in place. A 600px slide is vestibular. A 200 ms opacity change is not, and neither is a color transition on a hover state, and neither is a nudge of a few pixels.

So the reduced version of an interface usually keeps the timing and the sequence and swaps the travel for a fade. Same information, none of the provocation.

Asked and answered

There’s a media query for this, and the elegant part is that it reads a setting the person has already turned on at the operating system level. You aren’t asking anybody to hunt for a toggle on your site, down in the footer next to the newsletter signup. They set it once, years ago, on their laptop, because everything was making them ill.

motion.css
.panel {
  transform: translateX(100%);
  transition: transform 300ms ease;
}

@media (prefers-reduced-motion: reduce) {
  /* same length of change, no travel */
  .panel {
    opacity: 0;
    transform: none;
    transition: opacity 200ms ease;
  }
}
CSS

The caveat is that as of this month that’s Safari and nothing else. Firefox has it in progress and Chrome hasn’t released it, so most of the people this would help won’t be getting it from you for a while, which I realize is not a thrilling pitch.

I’d write it anyway, for two reasons. It costs about a minute, and things that cost about a minute are exactly the things that never get added later. And my hunch is that somebody on macOS or iOS with Reduce Motion already switched on is disproportionately likely to be the person who needs it, because nobody goes looking for that setting recreationally.

It’s the same bet I made about Grid a year ago, except the downside here is zero. A browser that doesn’t understand the query ignores the whole block, and everybody keeps exactly the animation they were already getting.

Nuke it from orbit

There’s a snippet going around that turns everything off at once, and as a safety net underneath a large existing site full of animation you didn’t write, it’s a defensible thing to drop in.

reduced.css
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }
}
CSS

I’d treat that as a backstop and not a plan though. Per component is better, mostly because the blunt version also kills the useful motion I spent two sections defending.

The query only reaches CSS, plus whatever JavaScript bothers to ask it, and plenty of the worst offenders aren’t styled at all.

An autoplaying carousel is motion nobody requested, on a timer. It’s bad for everybody else too. I have never once seen analytics that justified one. If you can’t get it killed outright, then at minimum stop it on hover and on focus, and give it a real pause control that a keyboard can reach.

Autoplaying video, same story. matchMedia is how you ask from a script.

motion.js
const still = window.matchMedia('(prefers-reduced-motion: reduce)');

if (still.matches) {
  video.removeAttribute('autoplay');
  carousel.pause();
}

still.addListener(e => e.matches && carousel.pause());
JavaScript

Then there’s scroll-jacking, which the query doesn’t cover at all, because there’s no animation to shorten. There’s no fix available for that one. You just don’t do it.

What actually moved the client

The parallax got cut. I explained why, the client agreed on the spot, and that surprised me, because I’d gotten myself ready to argue about aesthetics. Instead I was telling them that a real customer might have to go and lie down, and nobody wants that on their homepage. A static hero went in with the headline fading up, and that fade sits inside the query too, along with everything else on the site that still moves.

The move that worked was the specific one. Not “this is bad practice,” which invites a conversation about whose practice and who’s paying for it, but “a person I work with lost an afternoon to this.” Specific beats principled in a meeting, most of the time.

No checker was going to catch it for me either. I wrote a couple years ago about a site where every image had an alt attribute and the audit came back clean. This is the same shape one floor down. The automated pass tells you about the markup, and a person standing at my elbow saying please turn that off told me about the person.

And I keep going back to the apology. They were the one who had to leave. They were the one who said sorry, because the page looked normal and therefore the problem must have been them. I suspect that’s the default for anybody whose body doesn’t match what a website has quietly assumed about bodies. Which means the odds of this ever arriving as a bug report are somewhere around zero, so there’s nothing to wait for.

Read similar posts
8 min

color-scheme and the white flash

A client's site was flashing white on every load for anyone whose operating system is dark, and the declaration that fixed it also closed the unreadable checkout dropdown I'd had filed under Chrome being weird for months.

8 min

I couldn't read it in the sun

I took my phone out to the front steps to look at a page I'd built, couldn't read a word of it, and then found that every pair of colors on it cleared the threshold with room to spare.