Media Queries and Breakpoints
In order to make it as declarative and easy to handle media queries from JavaScript, you may be interested to use both the MediaQuery
React component and the useMediaQuery
React hook.
Media Queries Properties Table
UX designers are using a 12 column system during their design processes.
Pixel | Type | Rem | Custom Property | Comments |
---|---|---|---|---|
640 | small | 40em | --layout-small | 4 columns |
960 | medium | 60em | --layout-medium | 6 columns |
1152 | large | 72em | --layout-large | 12 columns |
Breakpoint ranges
Application in DNB do actually break only twice (small
and medium
). But have a HTML body max-width of large
.
Name | Range | Mixin | Columns |
---|---|---|---|
isSmall | from 0 to 40em | allBelow(small) | 4 |
isMedium | from 40em to 60em | allBetween(small, medium) | 6 |
isLarge | from 60em | allAbove(medium) | 12 |
So when dealing with naming of breakpoint ranges (between breakpoints), we actually use the term "large" when a media query exceeds medium
:
Here is how ranges breakes down in pixels:
- The small range goes from 0 to 640px
- The medium range goes from 641px to 960px
- The large range goes from 961px to infinity
UX Design and Breakpoints
When dealing with breakpoints; UX often designs only for two sizes. This leads with an unknown size in between breakpoints. So check with your UXer your applications should behave for when the screen size is in between.
MediaQuery component and React Hooks
Both the component and the React Hooks uses the JavaScript API matchMedia.
-
useMedia React Hook for screen width only.
-
useMediaQuery React Hook for all kinds of media queries.
-
MediaQuery Component for all kinds of media queries.
Re-render and performance
By using matchMedia
we only render when the requested media query actually changes. So we do not need to listen to e.g. window.addEventListener('resize', ...)
which is a performance waste, even with a debounce helper.
CSS similarity
It uses the same query API as CSS uses. You are able to provide your query also raw, by using e.g. query="(min-width: 60em)"
. But your custom queries will quickly grow and mess up your application code unnecessarily.
Properties
You can both use min
and max
, they are equivalent to minWidth
and maxWidth
.
CamelCase properties will be converted to kebab-case.
SSR
During a SSR (Server Side Render) we do not have the clients window.matchMedia
. In order to make the initial render to a positive match, you can set the matchOnSSR={true}
property.
Units
Numeric values will be handled as an em
unit.
useMedia
hook usage
The useMedia
hook acts like a switch, where only one of the properties will be true at a time.
import { useMedia } from '@dnb/eufemia/shared'function Component() {const { isSmall, isMedium, isLarge, isSSR } = useMedia()return isSmall && <IsVisibleWhenSmall />}
The returned constants like isLarge
etc. are within "breakpoint ranges" – likewise the SCSS mixins such as allAbove
etc.
See the table above for the available breakpoints and their corresponding media queries.
SSR (Server Side Render) usage
To lower the possibility of CLS (Cumulative Layout Shift) on larger screens – you can make use of the isSSR
property. Try to use it in combination with isLarge
, because the negative CLS experience is most recognizable on larger screens:
import { useMedia } from '@dnb/eufemia/shared'function Component() {const { isSmall, isMedium, isLarge, isSSR } = useMedia()return (isLarge || isSSR) && <IsVisibleDuringSsrAndWhenLarge />}
During SSR, when no window
object is available, all results are negative. But you can provide a initialValue
:
import { useMedia } from '@dnb/eufemia/shared'function Component() {const { isSmall } = useMedia({initialValue: {isSmall: true,},})return isSmall && <IsVisibleDuringSSR />}
Here are all the options:
import { useMedia } from '@dnb/eufemia/shared'function Component() {const { isSmall } = useMedia({/*** Give a initial value, that is used during SSR as well.* Default: null*/initialValue?: Partial<UseMediaResult>/*** If set to true, no MediaQuery will be used.* Default: false*/disabled?: boolean/*** Provide a custom breakpoint* Default: defaultBreakpoints*/breakpoints?: MediaQueryBreakpoints/*** Provide a custom query* Default: defaultQueries*/queries?: Record<string, MediaQueryCondition>/*** For debugging*/log?: boolean})return isSmall}
{
"isSmall": false,
"isMedium": false,
"isLarge": false,
"isSSR": true,
"innerWidth": 0
}
You can disable the usage of window.matchMedia
by providing useMedia({ disabled: true })
.
You can log the media query by providing useMedia({ log: true })
.
useMediaQuery
hook usage
This React Hook is a more extended version, where you can define all sorts of Media Queries.
import { useMediaQuery } from '@dnb/eufemia/shared'// orimport useMediaQuery from '@dnb/eufemia/shared/useMediaQuery'function Component() {const match = useMediaQuery({matchOnSSR: true,when: { min: 'medium' },})return match ? 'true' : 'false'}
You can disable the usage of window.matchMedia
by providing useMedia({ disabled: true })
.
Live example
This example uses the not
property to reverse the behavior.
MediaQuery
component
import { MediaQuery } from '@dnb/eufemia/shared'// orimport MediaQuery from '@dnb/eufemia/shared/MediaQuery'
You have plenty of possibilities to mix and match:
<MediaQuery when={{ min: 'medium' }}>matches all above medium screens</MediaQuery><MediaQuery when={{ screen: true, orientation: 'landscape' }}>matches orientation landscape screens</MediaQuery><MediaQuery not when={{ min: 'large' }}>matches all, but beneath large screens</MediaQuery><MediaQuery matchOnSSR when={{ min: 'small', max: 'medium' }}>matches small and medium screens and during SSR</MediaQuery><MediaQuery when={[{ min: 'small', max: 'large' }, { print: true }]}>matches all between small and large screens or all print media</MediaQuery><MediaQuery when={{ max: '60em' }}>matches screens to a max of 60em</MediaQuery><MediaQuery query="(min-width: 40em) and (max-width: 72em)">matches screens between 40em and 72em</MediaQuery>
you can find the properties on this page.
Interceptor on change listener
import { onMediaQueryChange } from '@dnb/eufemia/shared/MediaQuery'const remove = onMediaQueryChange({ min: 'medium' }, (match, event) => {// callback})// Will remove the listenersremove()
Use different breakpoints
It is possible to change the used breakpoint types by providing them to the Eufemia Provider.
Both the MediaQuery
component and the hooks useMedia
and useMediaQuery
will merge and use these custom breakpoints.
NB: It should be done only temporary, because DNB should align on one set of breakpoints for best UX and consistency.
import { Provider } from '@dnb/eufemia/shared'...<Providervalue={{breakpoints: {small: '40em',medium: '60em',large: '72em',},}}><App /></Provider>
Import breakpoints into JavaScript
You get an object with the values and the types as the keys.
import { defaultBreakpoints } from '@dnb/eufemia/shared/MediaQueryUtils'
SASS / SCSS mixins
You can re-use the SASS mixins from Eufemia:
// breakpoints.scss@import '@dnb/eufemia/style/core/utilities';$layout-small: map-get($breakpoints, 'small');$layout-medium: map-get($breakpoints, 'medium');$layout-large: map-get($breakpoints, 'large');
or like this:
@import '@dnb/eufemia/style/core/utilities';@include allBelow(large) {/* Your CSS */}@include allAbove(small) {/* Your CSS */}
Media Queries Examples
@media screen and (max-width: 40em) {/* small */}@media screen and (max-width: 60em) {/* medium */}@media screen and (max-width: 72em) {/* large */}
Based of the findings of this article and this webkit bug Eufemia recommends to use em
units for media query usage to meet the best overall browser support. Read more about units.
How to deal with Jest
You can mock window.matchMedia
with e.g. jest-matchmedia-mock.
import MatchMediaMock from 'jest-matchmedia-mock'const matchMedia = new MatchMediaMock()it('your test', () => {matchMedia.useMediaQuery('(min-width: 40em) and (max-width: 60em)')...})