useReducedMotion
useReducedMotion returns a $state-backed { current } object that reflects the user’s prefers-reduced-motion accessibility setting. .current updates live when the media query changes, so components can disable or simplify animations the moment the user toggles the OS preference.
<script>
import { useReducedMotion } from '@humanspeak/svelte-motion'
const reduced = useReducedMotion()
</script>
<div style:transform={reduced.current ? 'none' : 'rotate(45deg)'}>
Respects the user's preference
</div><script>
import { useReducedMotion } from '@humanspeak/svelte-motion'
const reduced = useReducedMotion()
</script>
<div style:transform={reduced.current ? 'none' : 'rotate(45deg)'}>
Respects the user's preference
</div>Diverges from React framer-motion’s plain
boolean | nullreturn for the same reason asuseCycle: a$state-backed value must live on an object so reads inside getters preserve tracking under Svelte 5 runes.
Why it matters
Some users disable motion at the OS level because animations cause vestibular discomfort, distraction, or other accessibility issues. useReducedMotion gives your components a single source of truth so they can opt out of motion gracefully rather than ignoring the user’s setting.
OS preference: no-preference
Tip: Chrome DevTools → Rendering → emulate prefers-reduced-motion: reduce to test the OS path.
Usage
Read reduced.current directly in templates, $derived, and $effect — it’s reactive via $state:
<script lang="ts">
import { useReducedMotion } from '@humanspeak/svelte-motion'
const reduced = useReducedMotion()
</script>
{#if reduced.current}
<p>Animations have been disabled to respect your preference.</p>
{:else}
<FancyAnimatedHero />
{/if}<script lang="ts">
import { useReducedMotion } from '@humanspeak/svelte-motion'
const reduced = useReducedMotion()
</script>
{#if reduced.current}
<p>Animations have been disabled to respect your preference.</p>
{:else}
<FancyAnimatedHero />
{/if}Skipping motion in motion components
Combine with motion to swap an animated transition for an instant change when reduced motion is requested:
<script lang="ts">
import { motion, useReducedMotion } from '@humanspeak/svelte-motion'
const reduced = useReducedMotion()
</script>
<motion.div
animate={{ x: 100 }}
transition={reduced.current ? { duration: 0 } : { type: 'spring', stiffness: 200 }}
/><script lang="ts">
import { motion, useReducedMotion } from '@humanspeak/svelte-motion'
const reduced = useReducedMotion()
</script>
<motion.div
animate={{ x: 100 }}
transition={reduced.current ? { duration: 0 } : { type: 'spring', stiffness: 200 }}
/>With variants
When you build variants, fall back to a “no motion” variant for users who opt out:
<script lang="ts">
import { motion, useReducedMotion } from '@humanspeak/svelte-motion'
const reduced = useReducedMotion()
const variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}
const reducedVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 }
}
</script>
<motion.div
variants={reduced.current ? reducedVariants : variants}
initial="hidden"
animate="visible"
/><script lang="ts">
import { motion, useReducedMotion } from '@humanspeak/svelte-motion'
const reduced = useReducedMotion()
const variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
}
const reducedVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1 }
}
</script>
<motion.div
variants={reduced.current ? reducedVariants : variants}
initial="hidden"
animate="visible"
/>How it works
- Subscribes to
window.matchMedia('(prefers-reduced-motion: reduce)')via$effect. - Uses
MediaQueryListchangeevents; falls back to the legacyaddListenerAPI for Safari < 14. - Listener is bound to the surrounding reactive scope (the component’s lifecycle) — detached automatically on unmount.
- Returns a static
{ current: false }in SSR or environments withoutmatchMedia, so it is safe to call during server rendering.
API Reference
Returns
A ReducedMotionState object:
currentboolean(getter) —truewhen the user has requested reduced motion, otherwisefalse. Reactive via$state.subscribe(run)— Svelte readable store contract. Synchronously emits the current value, then re-emits on every change. Kept for compat with hooks that still consume Svelte readables; prefer.currentfor new code.
Testing the preference
You don’t have to change OS settings to verify your reduced-motion code paths:
- Chrome / Edge DevTools: open DevTools → ⋯ → More tools → Rendering → Emulate CSS media feature
prefers-reduced-motion→reduce. - Firefox: set
ui.prefersReducedMotionto1inabout:config. - Playwright:
test.use({ reducedMotion: 'reduce' })orawait page.emulateMedia({ reducedMotion: 'reduce' })in a test.
See also
- useTime - Drive animations from a reactive time store
- useAnimationFrame - Frame-by-frame control
- WCAG 2.3.3 Animation from Interactions
Based on Motion’s useReducedMotion API.