Responsive styling and viewport dependant JavaScript.

Prefer CSS media queries. CSS applies during server-side rendering, while matchMedia only works in the browser. Rendering different markup per screen range in JavaScript causes hydration errors or layout shifts.

#Screen Ranges

These ranges represent the most common ones we use for responsive design.

Test

#Screen Range Media Queries

Media queries for CSS that is only applied within. All range names are inclusive, for example mobileTabletDesktop means mobile and tablet and desktop.


Queryrange startrange end
screenRangeQueries.mobile0px699px
screenRangeQueries.tablet700px991px
screenRangeQueries.desktop992px1839px
screenRangeQueries.widescreen1840pxInfinity
screenRangeQueries.mobileTablet0px991px
screenRangeQueries.tabletDesktop700px1839px
screenRangeQueries.desktopWidescreen992pxInfinity
screenRangeQueries.mobileTabletDesktop0px1839px
screenRangeQueries.tabletDesktopWidescreen700pxInfinity
import { screenRangeQueries } from "@blocks/theme";

const ExampleComponent = styled.div`
  /* mobile first */
  background-color: green;

  /* 992px to Infinity */
  ${screenRangeQueries.desktopWidescreen} {
    background-color: purple;
  }
`;

#Reading the Screen Range in JavaScript

isScreenRange and isMediaQuery return whether a query matches at the moment they are called. Use them where the value is read on demand: event handlers, tracking, or effects triggered by something else. They need no hook, state or listener, and return false during SSR.

isMediaQuery accepts a plain condition or a query from screenRangeQueries / breakpointRangeQueries.

import {
  breakpointRangeQueries,
  isMediaQuery,
  isScreenRange,
} from "@blocks/theme";

const handleClick = () => {
  if (isScreenRange("mobile")) {
    scrollToPosition(0, savedScrollY);
  }
};

const handleImpression = () => {
  track({ columnCount: isMediaQuery(breakpointRangeQueries["md-*"]) ? 3 : 2 });
};

const handlePointerUp = () => {
  if (!isMediaQuery("(prefers-reduced-motion: reduce)")) {
    startMomentumScroll();
  }
};

Don't use them for render output. They return false on the server, so the first client render can differ from the server markup.

#Screen Range Effect Hook

The useScreenRangeEffect runs JavaScript when a screen range starts or stops matching, for example to close a menu when the viewport leaves mobileTablet. Use with caution: setting state in the callback re-renders the component.

Callback arguments

  • isInRange is true and false according to matching the range
  • reason shows the phase of when it was matched, "mount" or "change"
import { useScreenRangeEffect } from "@blocks/theme";

const Component = () => {
  useScreenRangeEffect(
    "mobileTablet", // screen range name
    (isInRange) => {
      if (!isInRange) {
        closeMobileMenu();
      }
    },
    [],
  );
};

Hint: For touch and mouse distinction, consider using useDeviceEffect().

#Breakpoints

Breakpoint values are accessible via breakpoints object from @blocks/theme.


Breakpointviewport width
xxs450px
xs700px
sm992px
md1200px
lg1400px
xl1600px
xxl1840px

#Breakpoint Media Queries

Use screenRangeQueries whenever possible, breakpointRangeQueries are for special cases where the ranges are not flexible enough.

// try to use screenRangeQueries whenever possible.
import { breakpointRangeQueries } from "@blocks/theme";

const ExampleComponent = styled.div`
  background-color: green;

  /* from 0px to 1199px */
  ${breakpointRangeQueries["*-md"]} {
    background-color: red;
  }
`;

#Syntax of the breakpoint queries

Wildcard based open boundaries.


Queryrange startrange enddescription
breakpointRangeQueries["xs-md"]700px1199pxeverything between xs (inclusive)
up to the md breakpoint (exclusive)
breakpointRangeQueries["*-md"]0px1199pxeverything from zero
up to the md breakpoint (exclusive)
breakpointRangeQueries["md-*"]1200pxInfinityeverything from md (inclusive)
to Infinity

#Breakpoint Range Effects

The useBreakpointRangeEffect hook is a simple way to listen to the breakpoint changes in JavaScript.

The callback arguments

  • isInRange is true and false according to matching the range
  • reason shows the phase of when it was matched, "mount" or "change"

For more flexibility, consider using useMediaQueryEffect() instead.

import { useBreakpointRangeEffect } from "@blocks/theme";
import { useState } from "react";

const Component = () => {
  const [inRange, setInRange] = useState(false);

  useBreakpointRangeEffect(
    "md-*", // breakpoint range query key
    (isInRange, reason) => {
      setInRange(isInRange);
    },
    [],
  );
};

#Media Query Effect Hook

The useMediaQueryEffect hook is a more flexible way to listen to the media query changes in JavaScript. It leverages the window.matchMedia API under the hood: MDN matchMedia.

import { useMediaQueryEffect } from "@blocks/theme";
import { useState } from "react";

const Component = () => {
  const [isLandscape, setIsLandscape] = useState(false);

  useMediaQueryEffect(
    "(orientation: landscape)", // media query
    (queryDidMatch) => {
      setIsLandscape(queryDidMatch);
    },
    [],
  );
};

const CustomQueryComponent = () => {
  // You can also use this hook to build custom screen range queries
  const [inRange, setInRange] = useState(false);

  // 992px - 1199px, without the "@media" prefix
  const customScreenRangeQuery = `(min-width: ${breakpoints.sm}px) and (max-width: ${breakpoints.md - 1}px)`;
  useMediaQueryEffect(customScreenRangeQuery, (queryDidMatch) =>
    setInRange(queryDidMatch),
  );
};