feat(MotionPreference): Create common logic to store information about prefers-reduced-motion

This commit is contained in:
Ilia Mashkov
2026-01-17 14:29:10 +03:00
parent 71d320535e
commit 32da012b26
3 changed files with 41 additions and 19 deletions

View File

@@ -0,0 +1,37 @@
// Check if we are in a browser environment
const isBrowser = typeof window !== 'undefined';
class MotionPreference {
// Reactive state
#reduced = $state(false);
#mediaQuery: MediaQueryList = new MediaQueryList();
private handleChange = (e: MediaQueryListEvent) => {
this.#reduced = e.matches;
};
constructor() {
if (isBrowser) {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
// Set initial value immediately
this.#reduced = mediaQuery.matches;
mediaQuery.addEventListener('change', this.handleChange);
this.#mediaQuery = mediaQuery;
}
}
// Getter allows us to use 'motion.reduced' reactively in components
get reduced() {
return this.#reduced;
}
destroy() {
this.#mediaQuery.removeEventListener('change', this.handleChange);
}
}
// Export a single instance to be used everywhere
export const motion = new MotionPreference();

View File

@@ -13,3 +13,5 @@ export {
type Virtualizer,
type VirtualizerOptions,
} from './helpers';
export { motion } from './accessibility/motion.svelte';

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import type { Filter } from '$shared/lib';
import { motion } from '$shared/lib';
import { Badge } from '$shared/shadcn/ui/badge';
import { buttonVariants } from '$shared/shadcn/ui/button';
import { Checkbox } from '$shared/shadcn/ui/checkbox';
@@ -37,29 +38,11 @@ const { displayedLabel, filter }: PropertyFilterProps = $props();
// Toggle state - defaults to open for better discoverability
let isOpen = $state(true);
// Accessibility preference to disable animations
let prefersReducedMotion = $state(false);
// Check reduced motion preference on mount (window access required)
// Event listener allows responding to system preference changes
onMount(() => {
if (typeof window !== 'undefined') {
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
prefersReducedMotion = mediaQuery.matches;
const handleChange = (e: MediaQueryListEvent) => {
prefersReducedMotion = e.matches;
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}
});
// Animation config respects user preferences - zero duration if reduced motion enabled
// Local modifier prevents animation on initial render, only animates user interactions
const slideConfig = $derived({
duration: prefersReducedMotion ? 0 : 250,
duration: motion.reduced ? 0 : 250,
easing: cubicOut,
});