Vestibular-Safe Animation Patterns: Why Some Motion Makes People Sick
A designer asks for a hero image that drifts as you scroll and a section that swings into view. Both are ordinary requests, both are easy to build, and both belong to the small category of web animation that can leave a reader dizzy, nauseated, or with a migraine that outlasts the visit. This page is about the perceptual mechanism behind that — what the visual system is doing when an animation provokes symptoms, and which properties of an effect predict whether it will. It sits under Accessibility in CSS Animations, and where the other motion pages here cover the preference and the conformance rules, this one covers the reason those rules exist.
What determines whether motion is provocative:
- How much of the visual field the moving content covers
- Whether the movement is coherent enough to imply self-motion
- Which axis it moves on, and whether it rotates
- Its duration, speed profile, and predictability
Vection: the illusion your interface can create
Your sense of your own movement is assembled from three inputs: the vestibular organs in the inner ear, which sense linear acceleration and rotation; proprioception from muscles and joints; and vision. Vision is weighted heavily. When a large region of the visual field moves coherently in one direction, the brain's most economical explanation is usually I am moving, not the world is moving. That induced illusion of self-motion is called vection, and it is the reason a stationary train feels like it is pulling away when the train on the next platform starts to move.
Vection is normally harmless. It becomes a problem when it conflicts with the other two inputs. The eyes report movement, the inner ear reports stillness, proprioception agrees with the inner ear, and the resulting mismatch is the same sensory conflict implicated in motion sickness. Symptoms in this state range from mild unease and eye strain through headache and disorientation to frank vertigo and nausea.
Two things make this a real design concern rather than an edge case. First, vestibular dysfunction is common: one widely cited US national survey found measurable vestibular dysfunction in around a third of adults aged 40 and over, and the figure rises steeply with age. People with vestibular migraine, persistent postural-perceptual dizziness, or a recovering inner-ear injury can be provoked by visual motion far weaker than would affect anyone else. Second, symptoms are often delayed and outlast the exposure, so the person who closes your site after a parallax hero may not connect the two — you will never see it in your analytics.
The design conclusion is not "no animation". It is that coverage, coherence, and displacement are the risk factors, not animation as a category.
The four variables that predict risk
Coverage. Vection strength scales with the proportion of the visual field the moving stimulus occupies, and peripheral vision contributes disproportionately. A full-bleed background that moves is the worst case; a 200-pixel card animating in the middle of a static page is a very different stimulus even at the same speed. This is why "make the hero animation subtle" often fails — subtlety of style does nothing if the moving area is still the whole screen.
Coherence. A field where everything moves the same way reads as self-motion. A field where one small element moves against a stable background reads as an object moving. Parallax is uniquely bad here precisely because it is coherent and it introduces a depth cue: differential speeds between layers are exactly the optic flow pattern produced by travelling through a scene.
Axis and rotation. Rotation is more provocative than translation at equivalent magnitudes, and rotation about the roll axis — the one a spinning page transition uses — is the most provocative of the three. Scaling is effectively motion along the depth axis; a large scale() reads as looming, an approach signal the visual system takes seriously. Vertical translation is generally worse than horizontal, because the page already scrolls vertically and the two combine.
Duration and profile. Brief movement gives the conflict little time to accumulate; sustained or looping movement gives it plenty. Scroll-linked motion is the extreme case, because it lasts exactly as long as the reader keeps scrolling and its speed is unpredictable. A sharp acceleration curve is more noticeable than a gentle one at the same total displacement, so easing choice matters — but easing cannot rescue an effect that is too large.
None of these has a number in any specification, and you should be suspicious of anyone who quotes one. What they give you is a set of dials to turn down, and a way to reason about a specific effect rather than banning a property.
Approach rationale: a motion budget, expressed in CSS
The practical form of all that is a budget: a small set of custom properties that cap displacement, and a rule that decorative animation may only spend from them. Doing it in tokens rather than per-component discipline means the cap is reviewable, and it means the reduced path has exactly one place to zero out.
The alternative — hand-tuning each animation and hoping reviewers catch the large ones — does not survive contact with a growing codebase. The tradeoff is that a token cannot know how much of the viewport a given element covers, so the budget handles displacement and you still have to make the coverage judgement yourself. A 12-pixel translate is comfortable on a card and is still 12 pixels of full-width movement when applied to a hero.
Complete working implementation
A content-reveal pattern with a declared budget. Movement is bounded relative to the element, never to the viewport, and rotation is not available to spend at all.
<section class="reveal">
<h2 class="reveal__title">Field measurements</h2>
<p class="reveal__body">Content that arrives without crossing the screen.</p>
</section>
:root {
/* The budget. Travel is capped at 10px or 2% of the element's own
inline size, whichever is smaller, so the cap tightens rather than
loosens as a component grows to fill more of the viewport. */
--travel-max: min(10px, 2%);
/* Scale is expressed as a delta from 1 so the cap is obvious at a
glance. 0.02 is a 2% size change: perceptible, not looming. */
--scale-delta: 0.02;
--ease-calm: cubic-bezier(0.33, 0, 0.2, 1); /* no overshoot */
}
.reveal {
opacity: 0;
/* Spend from the budget rather than picking a number here. */
translate: 0 var(--travel-max);
scale: calc(1 - var(--scale-delta));
animation: reveal-in 450ms var(--ease-calm) forwards;
}
@keyframes reveal-in {
to {
opacity: 1;
translate: 0 0;
scale: 1;
}
}
/* Opacity is the one channel that produces no optic flow at all, so it
is the only thing left when the budget is set to zero. */
@media (prefers-reduced-motion: reduce) {
:root {
--travel-max: 0px;
--scale-delta: 0;
}
}
Setting the tokens to zero in the reduce block is enough — translate: 0 0px and scale: 1 are the element's resting values, so the keyframe interpolates between two identical geometries and only opacity visibly changes. No component-level override is needed anywhere.
For contrast, the shape of an effect that spends far outside any budget, shown so it can be recognised in a review:
/* AVOID: coherent, full-bleed, long-travel, and it scales.
Every one of the four risk variables is at its maximum. */
.hero__bg {
animation: hero-swoop 1.2s ease-out;
}
@keyframes hero-swoop {
from { translate: 0 -220px; scale: 1.6; rotate: -4deg; }
to { translate: 0 0; scale: 1; rotate: 0deg; }
}
The technique that makes it work
min(10px, 2%) is the detail that turns a guideline into an enforced cap. A fixed 12px translate is comfortable on a card and becomes a disproportionately visible movement when the same token is applied to something large — but it is comfortable because 12 pixels is small relative to a card, and that relationship is what you actually want to preserve. Expressing the cap as min() of an absolute and a percentage means the percentage takes over for large elements: a 200-pixel card gets 4 pixels of travel, a 1200-pixel hero gets 10, never more. The dangerous direction — displacement growing with element size — is the one the min() closes off.
The second half is using the individual translate, scale and rotate properties rather than the transform shorthand. Because they are separate properties, the budget can neutralise displacement without touching anything else, and — more usefully in review — the absence of a --rotate-max token means there is no sanctioned way to spend rotation. A reviewer seeing a literal rotate: value in a component knows immediately that it came from outside the system.
Variation: cross-fading between states
When two views must swap — a tab panel, a theme change, a filtered result set — cross-fade rather than slide. Nothing crosses the retina, so no optic flow is generated regardless of how large the panels are, which makes this the one transition that stays safe at full-screen scale:
.panel {
opacity: 0;
transition: opacity 250ms var(--ease-calm);
}
.panel[data-active="true"] { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
.panel { transition-duration: 120ms; } /* shorten; the fade is safe */
}
If a directional hint is genuinely needed — a carousel where the reader must know whether they went forward or back — pair a full-strength fade with a token-capped translate so the opacity carries the transition and the few pixels of travel only disambiguate direction. This is also the right approach for view transitions with reduced motion, where the default cross-document animation is a slide.
Browser support
The individual translate, scale and rotate properties used throughout are supported in Firefox 72+, Safari 14.1+, Chrome 104+ and Edge 104+; on older Chromium the equivalent transform: translateY() scale() works identically, with the loss of independent neutralisation. min() in a length context is available across all current engines. prefers-reduced-motion is supported in Safari 10.1+, Firefox 63+, Chrome 74+ and Edge 79+. Because the budgeted defaults are already gentle, an engine that ignores the media query still renders something comfortable — the fallback is a design property here, not a technical one.
FAQ
What kinds of motion trigger vestibular symptoms? Movement that fills a large part of the visual field and moves coherently in one direction is the main risk, because it creates an illusion of self-motion the inner ear does not confirm. Parallax scrolling, big zooms, rotation and long slides are the usual offenders.
Is opacity safe for people with vestibular disorders?
Generally yes. A fade changes how visible something is without shifting it across the retina, so it produces no optic flow and no illusion of self-motion. Cross-fades and colour shifts are among the least provocative animated transitions.
Do I still need prefers-reduced-motion if my animations are small?
Yes. Individual sensitivity varies enormously, and thresholds that feel generous to one person are provocative to another. A small, well-budgeted animation is a better default, not a substitute for honouring the preference.
How large is too large for a transform animation? There is no threshold in the specification, but three variables predict risk together: how much of the viewport the moving content covers, how far it travels relative to its own size, and whether it rotates or scales. Keep travel to a small fraction of the element and avoid rotation in decorative effects.
Related
- Accessibility in CSS Animations — the parent guide on accessible motion.
- Reducing Motion Preferences in CSS — the media feature that switches the budget off.
- prefers-reduced-motion Recipes — per-effect reductions for the patterns named here.
- WCAG Motion Success Criteria — where the seizure and moving-content rules sit relative to this comfort guidance.
- Fluid Type Accessibility and Zoom — scaling text calmly without animated zoom.
Related articles
More pages in the same section.