CSS media queries are powerful. We can create rich, accessible experiences for users that change along with their devices, needs, and preferences. But sometimes CSS won’t cut it — and we need access to media queries directly in our React component.
Here’s how I used React Context to accomplish exactly this!
We start by defining the media queries we want to access:
const mediaQueries = {
xxsMax: '(max-width: 374px)',
xs: '(min-width: 0)'
xsMax: '(max-width: 479px)',
sm: '(min-width: 480px)',
smMax: '(max-width: 667px)',
md: '(min-width: 668px)',
mdMax: '(max-width: 991px)',
lg: '(min-width: 992px)',
lgMax: '(max-width: 1199px)',
xl: '(min-width: 1200px)',
portait: '(orientation: portrait)',
darkMode: '(prefers-color-scheme: dark)',
reduceMotion: '(prefers-reduced-motion: reduce)',
highContrast: '(prefers-contrast: more)',
};
Next, create your media-queries-context.tsx. This approach uses window.matchMedia and will monitor for any changes:
import React, {
useState,
useEffect,
createContext,
useContext,
ReactNode,
} from 'react';
import { mediaQueries } from './design-tokens';
const defaultValue = {};
interface MediaQueriesContextProps {
xs?: boolean;
xsMax?: boolean;
sm?: boolean;
smMax?: boolean;
md?: boolean;
mdMax?: boolean;
lg?: boolean;
lgMax?: boolean;
xl?: boolean;
portait?: boolean;
darkMode?: boolean;
highContrast?: boolean;
reduceMotion?: boolean;
}
const MediaQueriesContext = createContext(defaultValue);
interface MediaQueriesProps {
children: ReactNode;
}
const MediaQueriesProvider = ({ children }: MediaQueriesProps) => {
const [queryMatch, setQueryMatch] = useState({});
useEffect(() => {
const mediaQueryLists = {};
const keys = Object.keys(mediaQueries);
let isAttached = false;
const handleQueryListener = () => {
const updatedMatches = keys.reduce((acc, media) => {
acc[media] = !!(
mediaQueryLists[media]?.matches
);
return acc;
}, {});
setQueryMatch(updatedMatches);
};
if (window?.matchMedia) {
const matches = {};
keys.forEach((media) => {
if (typeof mediaQueries[media] === 'string') {
mediaQueryLists[media] = window.matchMedia(mediaQueries[media]);
matches[media] = mediaQueryLists[media].matches;
} else {
matches[media] = false;
}
});
setQueryMatch(matches);
isAttached = true;
keys.forEach((media) => {
if (typeof mediaQueries[media] === 'string') {
mediaQueryLists[media].addListener(handleQueryListener);
}
});
}
return () => {
if (isAttached) {
keys.forEach((media) => {
if (typeof mediaQueries[media] === 'string') {
mediaQueryLists[media].removeListener(handleQueryListener);
}
});
}
};
}, []);
return (
<MediaQueriesContext.Provider value={queryMatch}>
{children}
</MediaQueriesContext.Provider>
);
};
const useMediaQueries = () => {
const context = useContext<MediaQueriesContextProps>(MediaQueriesContext);
if (context === defaultValue) {
throw new Error('useMediaQueries must be used within MediaQueriesProvider');
}
return context;
};
export { useMediaQueries, MediaQueriesProvider };
Then, we’ll wrap the app (or component) in MediaQueriesProvider:
import { MediaQueriesProvider } from '../../common-components/media-queries-context';
const MyApp = ({ FooBarComponent, pageProps }) =>
<MediaQueriesProvider>
<FooBarComponent {...pageProps} />
</MediaQueriesProvider>
);
export default MyApp;
Finally — as long as our React components are children of this Provider — we can access these new values using the useMediaQueries hook:
import { useMediaQueries } from '../../../common-components/media-queries-context';
const FooBarComponent = () => {
const {
xs,
xsMax,
sm,
smMax,
md,
mdMax,
lg,
lgMax,
xl,
portait,
darkMode,
highContrast,
reduceMotion,
} = useMediaQueries();
return <>...</>;
};
export default FooBar;
Here’s to building fully accessible experiences! 🥂