useMediaQuery
useMediaQuery hook: respond to viewport changes and prefers-reduced-motion for responsive, accessible product tours
useMediaQuery
Reactive hooks for CSS media queries. Includes useMediaQuery for custom queries and usePrefersReducedMotion for accessibility.
Why Use These Hooks?
Tours need to adapt to user preferences and device capabilities:
- Responsive layouts - Change tour card position on mobile vs desktop
- Reduced motion - Disable animations for users who prefer less motion
- Dark mode - Detect system color scheme preferences
- Print styles - Hide tours when printing
useMediaQuery
Subscribe to any CSS media query and get reactive updates.
Usage
import { useMediaQuery } from '@tour-kit/core';
function ResponsiveTour() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
const isDesktop = useMediaQuery('(min-width: 1025px)');
return (
<TourCard
placement={isMobile ? 'bottom' : 'right'}
className={isMobile ? 'w-full' : 'w-80'}
>
{isMobile && <p>Swipe to navigate</p>}
{isDesktop && <p>Use arrow keys to navigate</p>}
</TourCard>
);
}Parameters
Prop
Type
Return Value
Prop
Type
usePrefersReducedMotion
A convenience hook for the prefers-reduced-motion media query.
Usage
import { usePrefersReducedMotion } from '@tour-kit/core';
function AnimatedOverlay() {
const prefersReducedMotion = usePrefersReducedMotion();
return (
<TourOverlay
className={prefersReducedMotion ? '' : 'animate-fade-in'}
style={{
transition: prefersReducedMotion ? 'none' : 'opacity 300ms ease',
}}
/>
);
}Return Value
Prop
Type
This hook is equivalent to useMediaQuery('(prefers-reduced-motion: reduce)').
Common Media Queries
Responsive Breakpoints
function ResponsiveExample() {
const isMobile = useMediaQuery('(max-width: 640px)');
const isTablet = useMediaQuery('(min-width: 641px) and (max-width: 1024px)');
const isDesktop = useMediaQuery('(min-width: 1025px)');
let placement: 'bottom' | 'right' | 'left' = 'bottom';
if (isTablet) placement = 'right';
if (isDesktop) placement = 'left';
return <TourCard placement={placement} />;
}Color Scheme
function ThemeAwareTour() {
const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
return (
<TourCard
className={prefersDark ? 'bg-gray-900 text-white' : 'bg-white text-gray-900'}
/>
);
}Pointer Type
function TouchOptimizedTour() {
const isTouch = useMediaQuery('(pointer: coarse)');
const hasFinePointer = useMediaQuery('(pointer: fine)');
return (
<TourNavigation>
{isTouch ? (
<button className="p-4 text-lg">Next</button>
) : (
<button className="px-3 py-1">Next</button>
)}
</TourNavigation>
);
}Print Media
function PrintAwareTour() {
const isPrinting = useMediaQuery('print');
// Hide tour completely when printing
if (isPrinting) return null;
return <Tour>{/* ... */}</Tour>;
}Accessibility: Reduced Motion
Always respect the prefers-reduced-motion preference:
import { usePrefersReducedMotion } from '@tour-kit/core';
function AccessibleTourCard() {
const prefersReducedMotion = usePrefersReducedMotion();
return (
<TourCard
// Disable animations
animate={!prefersReducedMotion}
// Or use CSS
className={prefersReducedMotion ? 'motion-reduce:transition-none' : ''}
// Inline styles
style={{
animationDuration: prefersReducedMotion ? '0s' : '0.3s',
transitionDuration: prefersReducedMotion ? '0s' : '0.2s',
}}
/>
);
}WCAG Requirement
WCAG 2.1 Success Criterion 2.3.3 requires respecting the user's motion preferences. Always use usePrefersReducedMotion when implementing animations.
SSR Handling
Both hooks handle SSR by defaulting to false:
// During SSR:
useMediaQuery('(max-width: 768px)'); // Returns false
usePrefersReducedMotion(); // Returns false
// After hydration:
// Hooks re-evaluate and return actual valuesThe initial SSR value is always false. If you need to handle the "unknown" state differently, check for hydration:
const [hydrated, setHydrated] = useState(false);
const isMobile = useMediaQuery('(max-width: 768px)');
useEffect(() => setHydrated(true), []);
if (!hydrated) return <LoadingState />;
return isMobile ? <MobileLayout /> : <DesktopLayout />;Combining Queries
function ComplexResponsiveLogic() {
const isSmallScreen = useMediaQuery('(max-width: 640px)');
const prefersReducedMotion = usePrefersReducedMotion();
const isTouch = useMediaQuery('(pointer: coarse)');
// Determine optimal tour experience
const config = {
placement: isSmallScreen ? 'bottom' : 'right',
animated: !prefersReducedMotion,
showSwipeHint: isSmallScreen && isTouch,
showKeyboardHint: !isTouch,
};
return (
<Tour>
<TourCard placement={config.placement} animate={config.animated}>
<TourCardContent />
{config.showSwipeHint && <p>Swipe left/right to navigate</p>}
{config.showKeyboardHint && <p>Use arrow keys to navigate</p>}
</TourCard>
</Tour>
);
}How It Works
- Initial Value - Returns
falseduring SSR, or the current match status client-side - Event Listener - Subscribes to
MediaQueryList.changeevents - Cleanup - Removes listener on unmount or query change
- Re-evaluation - Updates state when media query changes (e.g., window resize)
// Simplified implementation
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(() => {
if (typeof window === 'undefined') return false;
return window.matchMedia(query).matches;
});
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
setMatches(mediaQuery.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [query]);
return matches;
}Related
- Accessibility Guide - Full WCAG compliance details
- Animations Guide - Custom transitions with reduced motion
- TourOverlay - Uses reduced motion internally