Motion & reduced motion
Animation is a first-class citizen in Pixelore UI — buttons press down, hearts pop in, dialogs zoom open. But movement that helps some users harms others, so every animation in the system has a reduced-motion equivalent.
The two-layer strategy
We respect prefers-reduced-motion at both the CSS layer and the JavaScript layer.
Layer 1 — CSS
The global stylesheet caps CSS animations and transitions to ~0ms for users who request reduced motion:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}This is the same pattern used by major systems like Tailwind UI and the BBC's GEL — a safety net that catches everything CSS-based.
Layer 2 — JavaScript
CSS-only animations can't be conditionally swapped for a different animation. But Motion animations can. Every animated Pixelore UI component calls useReducedMotion() and chooses between two variants:
import { useReducedMotion } from "@pixelore/react"
import { motion } from "motion/react"
function PressableButton() {
const reduced = useReducedMotion()
return (
<motion.button
whileHover={reduced ? { opacity: 0.92 } : { x: -1, y: -1 }}
whileTap={reduced ? { opacity: 0.85 } : { x: 2, y: 2 }}
>
Press
</motion.button>
)
}The full-motion variant uses translate. The reduced variant uses opacity. The component still gives feedback; it just stops moving.
This pattern follows WCAG 2.3.3 (Animation from Interactions, Level AAA) — any non-essential motion can be disabled by user setting.
What about decorative animation?
The optional CRT scanline overlay (.po-scanlines) is purely decorative. We disable its background pattern entirely under prefers-reduced-motion, because static scanlines plus head movement could still trigger discomfort.
Hooks you can use
import { useReducedMotion, usePressAnimation } from "@pixelore/react"useReducedMotion()— returnstruewhen the user has opted in to reduced motion.usePressAnimation()— returns a ready-to-use MotionVariantsobject withidle,hover, andpressedstates. The reduced variant uses opacity; the full variant uses translate.
Building your own animated components
When extending Pixelore UI:
- Default to no animation, then opt in when you need feedback.
- Never animate
transformortop/left/right/bottomas the only feedback. Always pair them with a non-motion cue (color change, opacity). - Avoid parallax or scroll-tied motion entirely — they're the worst offenders for vestibular triggers.
- Test it. macOS: System Settings → Accessibility → Display → Reduce Motion. Windows: Settings → Accessibility → Visual Effects → Animation Effects.
Why this matters
About 35% of adults over 40 experience vestibular symptoms triggered by motion. For some, watching a button slide can cause nausea, dizziness, or migraine. Reduced-motion support isn't a polish item — it's the difference between "this site is fun" and "this site is unusable".
8-bit aesthetics already give us license to use stepped, snappy animations rather than long graceful eases. Lean into that.