A tiny (yet powerful) system for when CSS media queries aren't enough.
CSS media queries are great for dynamically changing styles based on screen size, but sometimes you need JavaScript values to respond to screen-size changes as well.
Check out this blog post for a deeper dive into the problem space!
Whenever you have a variable that should change its value based on the size of the screen, you use useResponsiveValue(defaultValue, overrides)
. The overrides
are based on your own custom screen sizes and names; this example uses the names "small", "medium", and "large".
const MyResponsiveComponent() {
const message = useResponsiveValue(
"Your screen is big", // default value
{ small: "Your screen is small" } // override on "small" screens
);
// works with any type of value
const number = useResponsiveValue(
100,
{ large: 50, medium: 25 }
);
...
}
# npm
npm install react-responsive-system
# yarn
yarn add react-responsive-system
Tip: to keep things organized, folks often create a new file called
responsiveSystem.js/ts
where they'll configure Responsive System.
The first step is to define your breakpoints. If you're not sure which values to use, the set of breakpoints in our example is a reasonable default.
/* responsiveSystem.js/ts */
const breakpoints = {
// --name them whatever you want--
sm: 600, // 0 - 600px -> "sm" (phones)
md: 1200, // 601 - 1200 -> "md" (tablets)
lg: Infinity, // 1201+ -> "lg" (laptops/desktops)
// --have as many as you'd like--
};
The values that you provide are the maximum pixel-widths for each named screen-size range ("screen class"). In order to make sure that all possible screen sizes are covered, there should be exactly one screen class with a maximum pixel-width of Infinity
.
/* responsiveSystem.js/ts */
import { createResponsiveSystem } from 'react-responsive-system';
const breakpoints = {
// your breakpoints here
};
const { ScreenClassProvider, useResponsiveValue } = createResponsiveSystem({
breakpoints,
});
export { ScreenClassProvider, useResponsiveValue };
We call createResponsiveSystem
with our own custom breakpoints, and we get back a ScreenClassProvider
(keeps track of the current screen class), and a hook called useResponsiveValue
(creates a value that changes based on the screen class).
Somewhere near the root of your app, add the ScreenClassProvider
. This component keeps track of the screen size and provides it to the rest of the app.
/* index.jsx/tsx */
import { ScreenClassProvider } from './responsiveSystem';
ReactDOM.render(
<ScreenClassProvider>
<App />
</ScreenClassProvider>,
document.getElementById('root'),
);
Now that the app is aware of the current screen class, we can declare values that dynamically change to fit any screen!
function ImageGallery() {
const imagesToShow = useResponsiveValue(
4 // show 4 images by default
{
md: 2, // on medium screens, show 2
sm: 1, // on small screens, only show 1
}
);
return <Gallery imagesToShow={imagesToShow} />
}
The first param is the default value (used when there are no applicable overrides) and the second param specifies the overrides. Check out the Cascading section to see how you can customize the behavior of these overrides.
By default, Responsive System overrides do not cascade, which is to say: if you add an override for screen class X
, those overrides will only be applied when the user's screen matches X
exactly. But in some cases, you might want to apply overrides on X
and also implicitly on some of your other screen classes.
This is where cascadeMode
comes in.
Our examples here will use 5 screen classes (
xl
>lg
>md
>sm
>xs
)
This is the default configuration. There is no implicit overriding in this mode.
const number = useResponsiveValue('default', { md: 'medium' });
Results on different screen sizes:
- xl: "default"
- lg: "default"
- md: "overridden"
- sm: "default"
- xs: "default"
In Desktop First mode, your overrides implicitly apply to everything smaller. Conceptually, you are specifying your default values for your largest screen class, and then making tweaks once the screen starts to get too small.
const number = useResponsiveValue('default', { md: 'overridden' });
Results on different screen sizes:
- xl: "default"
- lg: "default"
- md: "overridden"
- sm: "overridden"
- xs: "overridden"
In Mobile First mode, your overrides implicitly apply to everything larger. Conceptually, you are specifying your default values for your smallest screen class, and then making tweaks once the screen starts to get big enough.
const number = useResponsiveValue('default', { md: 'overridden' });
Results on different screen sizes:
- xl: "overridden"
- lg: "overridden"
- md: "overridden"
- sm: "default"
- xs: "default"
If you'd like to enable desktop/mobile-first cascading, you can pass the cascadeMode
option to createResponsiveSystem
.
export const { ScreenClassProvider, useResponsiveValue } = createResponsiveSystem({
breakpoints,
cascadeMode: 'mobile-first', // or "desktop-first"
});
Server-Side rendering is a bit tricky because when we're on the server we don't have direct access to the user's screen. Since we can't determine the screen class automatically, you must "manually" provide an initialScreenClass
for Responsive System to render.
export const { ScreenClassProvider, useResponsiveValue } = createResponsiveSystem({
breakpoints,
initialScreenClass: 'lg', // only add this if you're doing Server-Side Rendering
});
Now here's the downside: if you guess the initial screen class incorrectly, your users may see a quick flash of the wrong content before React kicks in (hydrates the app) and adjusts to the proper screen class. In order to avoid that experience, you have a few options:
If you're reading this section, you're probably already committed to server-side rendering your app, but I'll mention it anyway: if you don't SSR, then you don't have to worry about manually figuring out the initial screen class. We can figure it out for you and we can make sure that there's no flash of an incorrect screen class.
You can render some placeholder content (like a spinner/skeleton/glimmer) during SSR and then render the correct content once the code gets to the client. Showing a loading experience is better than showing incorrect/broken content and then shifting it.
Here's a quick implementation that shows how to detect whether you're rendering on the server or the client.
function ClientOnly({ placeholder, children }) {
const [isOnClient, setIsOnClient] = useState(false);
useEffect(() => {
setIsOnClient(true);
}, []);
return isOnClient ? children : placeholder;
}
If you're using this pattern, I recommend adapting it to use React Context so that you can determine
isOnClient
once and provide that context for the lifetime of the app vs having every new component start off asfalse
only to immediately switch totrue
.
You can try to do some clever things with your server in order to improve your chances of guessing the correct initial screen class.
This route is tricky, but if you want to try it, here are some ideas:
- You can check the User-Agent Request header to get an idea of what type of device is being used (mobile vs laptop/desktop) and use that info to make an educated guess. Check out ua-parser-js for parsing user agents on Node servers.
- If your user is navigating from page to page in your app and you control the links, you could append width and height query params or custom headers to each request before it goes out. This is a lot of work, but technically possible.
Want direct access to the screen class itself? Sure! We're already providing it via context, so here's a hook to get the raw value for your own purposes.
/* responsiveSystem.js/ts */
// ...
export const {
ScreenClassProvider,
useResponsiveValue,
useScreenClass, // export the hook
} = createResponsiveSystem(...);
/* button.js/ts */
import { useScreenClass } from '../responsiveSystem';
const Button = props => {
const screenClass = useScreenClass();
// screenClass will be a string representation of one of your breakpoints e.g. "xs", "mobile", etc.
// ...
};
For folks who are curious about the implementation:
Fundamentally, we need our components to be aware of the current screen class so they know when to re-render. We do this by constructing Media Queries from your breakpoints and using window.matchMedia
to observe changes, then we provide the current screen class to components via React Context. This will only trigger a re-render when the screen class actually changes, and not during resizing events within the same screen class.
Responsive System uses strict Semantic Versioning which means that our version numbers carry extra meaning. The general form is:
v{major}.{minor}.{patch}
except when the first number is 0, in which case it's v0.{major}.{minor}
patch
- if this number changes, then we've shipped a bug fix or small tweakminor
- if this number changes, then we've shipped a new feature that's backwards compatiblemajor
- if this number changes, then we've shipped a change that's not backwards compatible so be careful when upgrading!
This library is currently in the v0.X.X range, so keep in mind that the middle number represents major breaking changes and that upgrading will require extra work
- Removed
defaultScreenClass
param - Added
initialScreenClass
param - Fixed SSR
useLayoutEffect
bug - Fixed hydration mismatch bug
Upgrading from v0.9?
- if your app is client-side rendered, just remove
defaultScreenClass
and don't addinitialScreenClass
- if your app is server-side rendered, replace
defaultScreenClass
withinitialScreenClass
and refer to the server-side rendering docs for further considerations
- Removed
responsive(Component)
- Removed
useResponsiveProps(props)
- Removed
deepMerge
dependency - Added
useResponsiveValue(defaultValue, overrides)
Upgrading from v0.8?
- v0.8 was stable for 2+ years, so if it's working for you then the upgrade might not be worthwhile
- the
createResponsiveSystem
setup remains the same - components must be upgraded on a case-by-case basis
// 0.8
const ResponsiveCarousel = responsive(Carousel);
<ResponsiveCarousel
slidesToShow={4}
showArrows={true}
small={{ slidesToShow: 2, showArrows: false }}
/>;
// 0.9+
const responsiveSlidesToShow = useResponsiveValue(4, { small: 2 });
const responsiveShowArrows = useResponsiveValue(true, { small: false });
<Carousel slidesToShow={responsiveSlidesToShow} showArrows={responsiveShowArrows} />;