Skip to content
SiteEmail

Gameface runs inside the game engine, not the operating system, which means OS accessibility settings like prefers-reduced-motion are not available. Motion sensitivity support must come from your own settings menu: a player toggles a preference, your JS applies a class to the document root, and CSS animation rules respond to that class. This article walks through building that system.

On web browsers, @media (prefers-reduced-motion: reduce) lets you conditionally disable animations based on the player’s OS accessibility setting. Because Gameface renders inside the game process and not in a standard browser context, this media query has no source signal to read. It will not trigger regardless of what the player has set in their OS.

The correct approach is to build motion preference support directly into the game settings:

  1. The player sets a “Reduced Motion” option in the settings menu.
  2. The game engine communicates this to the UI via engine.trigger.
  3. JavaScript adds or removes a class on the <html> element.
  4. CSS rules scoped to that class disable or simplify animations.

A single class on the document root is the control point for all reduced-motion styles:

function setReducedMotion(enabled) {
if (enabled) {
document.documentElement.classList.add('motion-reduced');
} else {
document.documentElement.classList.remove('motion-reduced');
}
}
engine.on('ReducedMotionChanged', (enabled) => {
setReducedMotion(enabled);
});
// Restore saved preference on load
const savedMotionPreference = engine.call('GetReducedMotionPreference') ?? false;
setReducedMotion(savedMotionPreference);

Once this is in place, every animation or transition in the codebase can be conditionally disabled by scoping it under .motion-reduced.


The most complete approach is to turn off all transitions and animations globally when reduced motion is active, then selectively restore only the ones that are safe or necessary:

.motion-reduced *,
.motion-reduced *::before,
.motion-reduced *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}

Using 0.01ms rather than 0ms ensures that animation events (animationend, transitionend) still fire, which matters if any of your JavaScript logic waits for those events before changing state.

For individual elements where you want fine-grained control rather than a global disable:

.screen-transition {
transition: opacity 0.3s ease, transform 0.3s ease;
}
.motion-reduced .screen-transition {
transition: opacity 0.15s ease;
transform: none;
}

This keeps the fade-in opacity transition (non-disorienting) but removes the slide transform (the source of motion).


Parallax involves moving background layers at different speeds as the player interacts with a menu, creating a sense of depth. For players with vestibular disorders, this is one of the most disorienting effects in game UI.

A typical parallax implementation updates element transforms via JavaScript:

function updateParallax(mouseX, mouseY) {
parallaxLayers.forEach((layer, index) => {
const depth = (index + 1) * 0.02;
const offsetX = (mouseX - viewportCenterX) * depth;
const offsetY = (mouseY - viewportCenterY) * depth;
layer.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
});
}

Guard this function against the reduced-motion state:

function updateParallax(mouseX, mouseY) {
if (document.documentElement.classList.contains('motion-reduced')) {
return;
}
parallaxLayers.forEach((layer, index) => {
const depth = (index + 1) * 0.02;
const offsetX = (mouseX - viewportCenterX) * depth;
const offsetY = (mouseY - viewportCenterY) * depth;
layer.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
});
}

When the motion class is present, layers stay at their static positions. The effect disappears without any visual glitch because the transforms are simply never updated.


Looping animations, such as idle shimmer effects, floating icons, or pulsing health bars, are persistent and therefore more likely to cause discomfort than single-play transitions.

Mark looping animations explicitly in CSS so they are easy to target:

.shimmer-effect {
animation: shimmer 2s ease-in-out infinite;
}
.float-animation {
animation: float 3s ease-in-out infinite alternate;
}
.pulse {
animation: pulse 1.5s ease-in-out infinite;
}
/* Disable all looping animations under reduced-motion mode */
.motion-reduced .shimmer-effect,
.motion-reduced .float-animation,
.motion-reduced .pulse {
animation: none;
}

Removing animation: none in reduced motion mode collapses these elements to their resting state. Ensure the resting state (no transform, no opacity shift) looks intentional and not broken before shipping.


Not all animations are decorative. Some communicate critical state changes: a health bar depleting, an objective completing, an item being picked up. Disabling these entirely degrades usability.

The distinction to apply: decorative motion (parallax, shimmer, idle floats) should be completely disabled; functional feedback (state transitions, health warnings, notification entrances) should be simplified to opacity fades or instant state changes.

/* Decorative: disable */
.motion-reduced .menu-background-parallax {
transform: none;
animation: none;
}
/* Functional: simplify to opacity only, keep the feedback */
.motion-reduced .health-bar-damage-flash {
animation: healthFlashReduced 0.2s ease-out;
}
@keyframes healthFlashReduced {
from { opacity: 0.5; }
to { opacity: 1; }
}

The in-game settings toggle should be part of the accessibility section, not buried in graphics options. Present it clearly:

<div class="settings-row" role="group" aria-labelledby="motion-label">
<span id="motion-label" class="settings-label">Reduced Motion</span>
<div class="toggle-control">
<button
class="toggle-btn"
id="motion-toggle"
aria-pressed="false"
aria-describedby="motion-desc"
tabindex="0"
>
Off
</button>
<span id="motion-desc" class="settings-hint">
Disables parallax and looping animations. Motion in gameplay is unaffected.
</span>
</div>
</div>

Update the button state when the preference changes:

const motionToggle = document.getElementById('motion-toggle');
motionToggle.addEventListener('click', () => {
const isEnabled = motionToggle.getAttribute('aria-pressed') === 'true';
const newValue = !isEnabled;
motionToggle.setAttribute('aria-pressed', String(newValue));
motionToggle.textContent = newValue ? 'On' : 'Off';
setReducedMotion(newValue);
engine.call('SaveReducedMotionPreference', newValue);
});

The aria-pressed attribute communicates the toggle state to TTS and screen readers. The descriptive hint text sets accurate player expectations: UI motion is reduced, but gameplay motion (camera shake, for example) is a separate setting.