UNPKG

@blocz/react-responsive

Version:

🔍 <Only /> displays some contents for a specific screen size

427 lines (321 loc) 16.2 kB
# @blocz/react-responsive <!-- omit in toc --> `@blocz/react-responsive` is inspired by the `.visible` classes from [bootstrap 4](https://getbootstrap.com/docs/4.0/migration/#responsive-utilities) (and `.hidden` classes from [bootstrap 3](https://getbootstrap.com/docs/3.3/css/#responsive-utilities-classes)): it lets you show or hide components based on the current screen size. [See changelog](https://github.com/bloczjs/react-responsive/blob/main/CHANGELOG.md) ## Table of contents <!-- omit in toc --> 1. [Installation](#installation) 2. [How to use](#how-to-use) 1. [`useMediaQuery()`](#usemediaquery) 2. [Media ranges](#media-ranges) 1. [Default media ranges](#default-media-ranges) 2. [Additional `Up` and `Down`](#additional-up-and-down) 3. [`useMediaRange()`](#usemediarange) 4. [`<Only>`](#only) 1. [`on` prop](#on-prop) 2. [`matchMedia`](#matchmedia) 3. [Render as component (deprecated)](#render-as-component-deprecated) 5. [Custom media ranges: `createMediaRanges()`](#custom-media-ranges-createmediaranges) 1. [Strictly typed](#strictly-typed) 2. [Stricter `<Only>`](#stricter-only) 3. [Units \& direction](#units--direction) 6. [`<MediaRangesProvider>` (deprecated)](#mediarangesprovider-deprecated) 1. [Add more media ranges](#add-more-media-ranges) 2. [Change default media ranges](#change-default-media-ranges) 3. [Units](#units) 4. [Direction](#direction) 3. [Comparison to other libraries](#comparison-to-other-libraries) 4. [`matchMedia` polyfill](#matchmedia-polyfill) 1. [Browser](#browser) 2. [Node](#node) 5. [React 16 / 17 support](#react-16--17-support) 6. [Deprecated APIs](#deprecated-apis) 7. [FAQ](#faq) ## Installation ```sh # pnpm pnpm add @blocz/react-responsive # yarn yarn add @blocz/react-responsive # npm npm install @blocz/react-responsive ``` ## How to use ### `useMediaQuery()` `useMediaQuery()` is a [hook](https://react.dev/reference/react/hooks) that detects if the given media query matches the current viewport. ```javascript import React from "react"; import { useMediaQuery } from "@blocz/react-responsive"; const App = () => { const isLandscape = useMediaQuery("(orientation: landscape)"); return <p>{isLandscape ? "Landscape mode" : "Portrait mode"}</p>; }; ``` [Learn more about CSS media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) ### Media ranges `@blocz/react-responsive` is based on the classic bootstrap breakpoints: `xs`, `sm`, `md`, `lg` and `xl`. Unlike Bootstrapwhere `xs`, `sm`, etc. are single breakpoints – `@blocz/react-responsive` uses **media ranges**: each name describes the interval _between_ two breakpoints, making ranges explicit and non-overlapping. See [Why media ranges instead of breakpoints?](https://github.com/bloczjs/react-responsive/blob/main/FAQ.md#why-media-ranges-instead-of-breakpoints) for more context. #### Default media ranges By default, the media ranges are: | Media range | From | To | | ----------- | -----: | -------: | | `xs` | 0px | 575px | | `sm` | 576px | 767px | | `md` | 768px | 991px | | `lg` | 992px | 1199px | | `xl` | 1200px | Infinity | This makes it fully explicit: a `lg` device is not `md` nor `xl`. #### Additional `Up` and `Down` Each media range also comes with `{mediaRange}Up` and `{mediaRange}Down` variants – covering everything above or below that breakpoint: | Media range | From | To | | ----------- | -----: | -------: | | `xsUp` | 0px | Infinity | | `smUp` | 576px | Infinity | | `mdUp` | 768px | Infinity | | `lgUp` | 992px | Infinity | | `xlUp` | 1200px | Infinity | | Media range | From | To | | ----------- | ---: | -------: | | `xsDown` | 0px | 575px | | `smDown` | 0px | 767px | | `mdDown` | 0px | 991px | | `lgDown` | 0px | 1199px | | `xlDown` | 0px | Infinity | > **Note:** `xsDown` is equivalent to `xs`, `xlUp` is equivalent to `xl`, and `xlDown`/`xsUp` match all screen sizes – these exist only for convenience. #### `useMediaRange()` `useMediaRange()` is a [hook](https://react.dev/reference/react/hooks) that detects if the given media range matches the current viewport. ```javascript import React from "react"; import { useMediaRange } from "@blocz/react-responsive"; const App = () => { const matchXl = useMediaRange("xl"); const matchMdDown = useMediaRange("mdDown"); const matchMdOrLg = useMediaRange("md lg"); return ( <ul> {matchXl && <li>Visible on every "large" device</li>} {matchMdDown && <li>Visible on every device smaller than or equal to "medium"</li>} {matchMdOrLg && <li>Visible on every "medium" or "large" device</li>} </ul> ); }; ``` #### `<Only>` `<Only>` is the component equivalent of `useMediaRange()` and `useMediaQuery()`: it renders its children only when the condition matches. ##### `on` prop The `on` prop behaves like `useMediaRange()`: it accepts a media range name (or a space-separated list of names) and makes `<Only>` render its children when any of the named ranges match. ```javascript import React from "react"; import { Only } from "@blocz/react-responsive"; const App = () => ( <React.Fragment> <Only on="xs">Only visible for extra small devices (portrait phones)</Only> <Only on="sm">Only visible for small devices (landscape phones)</Only> <Only on="md">Only visible for medium devices (tablets)</Only> <Only on="lg">Only visible for large devices (desktops)</Only> <Only on="xl">Only visible for extra large devices (large desktops)</Only> <Only on="sm xl">Only visible for small AND extra large devices</Only> </React.Fragment> ); ``` ##### `matchMedia` The `matchMedia` prop behaves like `useMediaQuery()`: it accepts any regular query supported by [window.matchMedia](https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia). ```javascript import React from "react"; import { Only } from "@blocz/react-responsive"; const App = () => ( <Only matchMedia="(min-device-width: 500px) and (orientation: landscape)"> Visible on every device bigger than "500px" and in landscape mode </Only> ); ``` > **Note:** If you use `on` AND `matchMedia` together, the component renders if **any** of the media ranges matches **OR** if the media query is fulfilled (not AND). ##### Render as component (deprecated) > ⚠️ Using the `as` prop on `<Only>` is **deprecated** and will be removed in v6.0.0. > This is not considered as type-safe The `as` prop makes `<Only>` render as a different element (any DOM tag or React component). Any props except `on`, `matchMedia`, and `as` are forwarded to it: ```javascript import React from "react"; import { Only } from "@blocz/react-responsive"; const App = () => ( <ul> <Only as="li" on="xs"> Only visible for extra small devices </Only> <Only as="li" on="sm"> Only visible for small devices </Only> <Only as="li" on="md"> Only visible for medium devices </Only> </ul> ); ``` The `as` prop also accepts React components: ```javascript import React from "react"; import { Only } from "@blocz/react-responsive"; const Custom = ({ title, children }) => ( <React.Fragment> <h3>{title}</h3> <p>{children}</p> </React.Fragment> ); const App = () => ( <React.Fragment> <Only as={Custom} title="xs" on="xs"> Only visible for extra small devices </Only> <Only as={Custom} title="sm" on="sm"> Only visible for small devices </Only> <Only as={Custom} title="md" on="md"> Only visible for medium devices </Only> </React.Fragment> ); ``` #### Custom media ranges: `createMediaRanges()` `createMediaRanges()` is the recommended way to customize the media ranges. It returns an object `{ useMediaRange, Only }` bound to the ranges you pass in, with end-to-end TypeScript types. ```javascript import { createMediaRanges, DEFAULT_MEDIA_RANGES } from "@blocz/react-responsive"; const { useMediaRange, Only } = createMediaRanges({ ...DEFAULT_MEDIA_RANGES, pxRange: [263, 863, { unit: "px" }], emRange: [20, 40, { unit: "em" }], }); ``` If you want to re-use the same defaults as the top-level `<Only>` & `useMediaRange()`, you'll need to import & use `DEFAULT_MEDIA_RANGES`. ##### Strictly typed The returned `useMediaRange()` accepts only the names that match the ranges you declared (plus the auto-generated `Up` and `Down` aliases). The passed string can hold a single name or a space-separated list, every media range will be typechecked: ```typescript useMediaRange("md"); // ✅ useMediaRange("pxRangeUp"); // ✅ useMediaRange("mdDown"); // ✅ useMediaRange("md pxRange"); // ✅ useMediaRange("invalid"); // ❌ TS error useMediaRange("md invalid"); // ❌ TS error – "md" is fine, "invalid" is not ``` This is also true for the returned `<Only>`: ```tsx <> <Only // ✅ on="md pxRange" > </Only> <Only // ❌ TS error on="lg invalid" > </Only> </> ``` ##### Stricter `<Only>` Unlike the top-level `<Only>`, the `<Only>` returned from `createMediaRanges()` does not support the `as` prop (and does not forward additional props to an inner element). ##### Units & direction Each range entry accepts one of these shapes: `[min, max]`, or `[min, max, { unit?, direction? }]`: ```javascript const { Only } = createMediaRanges({ pxRange: [263, 863, { unit: "px" }], emRange: [20, 40, { unit: "em" }], yRange: [200, 400, { direction: "height" }], }); ``` #### `<MediaRangesProvider>` (deprecated) > ⚠️ `<MediaRangesProvider>` is **deprecated** and will be removed in v6.0.0. Use [`createMediaRanges()`](#custom-media-ranges-createmediaranges) instead. `<MediaRangesProvider>` defines all media range values. Use it to inject or modify the media ranges (only use one `<MediaRangesProvider>` per build). ##### Add more media ranges ```javascript import React from "react"; import { Only, MediaRangesProvider } from "@blocz/react-responsive"; const App = () => ( <MediaRangesProvider additionalMediaRanges={{ customRange: [263, 863] }}> <Only on="customRange">Visible on every device from "263px" to "863px"</Only> <Only on="customRangeUp">Visible on every device bigger than "263px"</Only> <Only on="customRangeDown">Visible on every device smaller than "863px"</Only> </MediaRangesProvider> ); ``` ##### Change default media ranges ```javascript import React from "react"; import { Only, MediaRangesProvider } from "@blocz/react-responsive"; const App = () => ( <MediaRangesProvider mediaRanges={{ sm: [263, 863] }}> <Only on="sm">Visible on every device from "263px" to "863px"</Only> <Only on="smUp">Visible on every device bigger than "263px"</Only> <Only on="smDown">Visible on every device smaller than "863px"</Only> </MediaRangesProvider> ); ``` **Warning**: This **overrides completely** the default media ranges, in this example, the other media ranges `xs`, `md`, `lg` and `xl` **are no longer defined!** ##### Units You can specify which unit is going to be used for the media range by specifying in the 3rd option a "unit" key. Every CSS unit is supported. The default unit is `px`. ```javascript import React from "react"; import { Only, MediaRangesProvider } from "@blocz/react-responsive"; const App = () => ( <MediaRangesProvider additionalMediaRanges={{ pxRange: [263, 863, { unit: "px" }], emRange: [20, 40, { unit: "em" }], }} > <Only on="pxRange">Visible on every device from "263px" to "863px"</Only> <Only on="emRange">Visible on every device from "20em" to "40em"</Only> </MediaRangesProvider> ); ``` ##### Direction You can specify which direction is used for the media queries (height or width). By default, "width" is the chosen direction. ```javascript import React from "react"; import { Only, MediaRangesProvider } from "@blocz/react-responsive"; const App = () => ( <MediaRangesProvider mediaRanges={{ xRange: [300, 500, { direction: "width" }], yRange: [200, 400, { direction: "height" }], }} > <Only on="xRange">Visible on every device from "300px" to "500px" wide</Only> <Only on="yRange">Visible on every device from "200px" to "400px" tall</Only> </MediaRangesProvider> ); ``` ## Comparison to other libraries | Lib | Media ranges | Custom media ranges | Media query | `matchMedia` listener\* | hooks | SSR support | | ------------------------------------------------------------------------------------- | -----------: | ------------------: | ----------: | ----------------------: | ----: | ----------: | | [@blocz/react-responsive](https://npmx.dev/package/@blocz/react-responsive) | | | | | | | | [react-responsive](https://npmx.dev/package/react-responsive) | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | | [react-breakpoints](https://npmx.dev/package/react-breakpoints) | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | | [react-responsive-breakpoints](https://npmx.dev/package/react-responsive-breakpoints) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | \*: `matchMedia` listener event means that the library is built around `matchMedia.addListener(callback)` and not `window.addEventListener('resize', callback)` (which is faster because the callback is only triggered when the media query's state changes and not at every resize). ## `matchMedia` polyfill ### Browser If you want to use `matchMedia` in browsers that don't support it, I'd recommend [`matchmedia-polyfill`](https://github.com/paulirish/matchMedia.js/). ### Node If you want to mock `matchMedia` on Node to execute tests for instance, you can use [`mock-match-media`](https://github.com/Ayc0/mock-match-media/). And if you need an example with `Jest`, `@testing-library/react`, `React` and `@blocz/react-responsive`, you can take a look at [these tests](https://github.com/bloczjs/react-responsive/blob/main/packages/tests/src/__tests__/ssr.ts). ## React 16 / 17 support `@blocz/react-responsive` relies on `useSyncExternalStore`. This function was added in React 18. If you are on React 16.8+ / React 17, you'll need to use [use-sync-external-store](https://npmx.dev/package/use-sync-external-store) to polyfill `useSyncExternalStore`. ## Deprecated APIs The terminology used by this library used to be "breakpoint". It was renamed to "media range" because each entry actually describes the range between two breakpoints rather than a single breakpoint. For backward compatibility, the previous exports are still available but marked as `@deprecated`, and will be removed in the next major release: | Deprecated | Replacement | | ---------------------------- | --------------------------------------------------------------- | | `useBreakpoint()` | `useMediaRange()` | | `<BreakpointsProvider>` | `<MediaRangesProvider>` | | `<BreakpointsContext>` | `<MediaRangesContext>` | | `breakpoints` prop | `mediaRanges` prop | | `additionalBreakpoints` prop | `additionalMediaRanges` prop | | `<MediaRangesProvider>` | [`createMediaRanges()`](#custom-media-ranges-createmediaranges) | | `<MediaRangesContext>` | [`createMediaRanges()`](#custom-media-ranges-createmediaranges) | ## FAQ For other questions, please take a look at our [FAQ document](https://github.com/bloczjs/react-responsive/blob/main/FAQ.md).