Most modern websites use responsive web design techniques to ensure they look good, are readable, and remain usable on devices with any screen size, i.e. mobile phones, tablets, laptops, desktop PC monitors, televisions, projectors, and more.

Sites using these techniques have a single template, which modifies the layout in response to the screen dimensions:

  • Smaller screens typically show a linear, single-column view where UI controls such as menus are activated by clicking (hamburger) icons.
  • Larger screens show more information, perhaps with horizontally-aligned sidebars. UI controls such as menu items may always be visible for easier access.

One big part of responsive web design is the implementation of a CSS or JavaScript media query to detect device size and automatically serve up the appropriate design for that size. We’re going to discuss why these queries are important and how to work with them, but first, let’s discuss responsive design in general.

Why Is Responsive Design Important?

It’s impossible to provide a single page layout and expect it to work everywhere.

When mobile phones first gained rudimentary web access in the early 2000s, site owners would often create two or three separate page templates loosely based around mobile and desktop views. That became increasingly impractical as the variety of devices grew exponentially.

Today, there are numerous screen sizes ranging from tiny wristwatch displays to huge 8 K monitors and beyond. Even if you only consider mobile phones, recent devices can have a higher resolution than many low-end laptops.

Mobile usage has also grown beyond that of desktop computers. Unless your site has a specific set of users, you can expect the majority of people to access it from a smartphone. Small-screen devices are no longer an afterthought and should be considered from the start, despite most web designers, developers, and clients continuing to use a standard PC.

Google has recognized the importance of mobile devices. Sites rank better in Google search when they’re usable and perform well on a smartphone. Good content remains vital, but a slow-loading site that fails to adapt to the screen dimensions of your userbase could harm your business.

Finally, consider accessibility. A site that works for everyone, no matter what device they’re using, will reach a larger audience. Accessibility is a legal requirement in many countries, but even if it’s not where you are, consider that more viewers will lead to more conversions and higher profitability.

How Does Responsive Design Work?

The basis of responsive design is media queries: a CSS technology that can apply styles according to metrics such as the output type (screen, printer, or even speech), screen dimensions, display aspect ratio, device orientation, color depth, and pointer accuracy. Media queries can also take user preferences into account, including reduced animations, light/dark mode, and higher contrast.

The examples we’ve shown demonstrate media queries using screen width only, but sites can be considerably more flexible. Refer to the full set of options on MDN for details.

Media query support is excellent and has been in browsers for more than a decade. Only IE8 and below have no support. They ignore styles applied by media queries, but this can sometimes be a benefit (read more in the Best Practices section below).

There are three standard ways to apply styles using media queries. The first loads specific stylesheets in HTML code. For example, the following tag loads the wide.css stylesheet when a device has a screen that is at least 800 pixels wide:

<link rel="stylesheet" media="screen and (min-width: 800px)" href="wide.css" />

Secondly, stylesheets can be conditionally loaded in CSS files using an @import at-rule:

/* main.css */
@import url('wide.css') screen and (min-width: 800px);

More typically, you’ll apply media queries in stylesheets using a @media CSS at-rule block that modifies specific styles. For example:

/* default styles */
main {
  width: 400px;
}

/* styles applied when screen has a width of at least 800px */
@media screen and (min-width: 800px) {
  main {
    width: 760px;
  }
}

Developers can apply whichever media query rules are necessary to adapt the layout of a site.

Media Query Best Practises

When media queries were first devised, many sites opted for a set of rigidly fixed layouts. This is conceptually easier to design and code because it effectively replicates a limited set of page templates. For example:

  1. Screen widths less than 600px use a 400px-wide mobile-like layout.
  2. Screen widths between 600px and 999px use a 600px-wide tablet-like layout.
  3. Screen widths greater than 1,000px use a 1000px-wide desktop-like layout.

The technique is flawed. The results on very small and very large screens can look poor, and CSS maintenance can be required as devices and screen sizes change over time.

A better option is to use a mobile-first fluid design with breakpoints that adapt the layout at certain sizes. In essence, the default layout uses the simplest small-screen styles that position elements in linear vertical blocks.

For example, <article> and <aside> inside a <main> container:

/* default small-screen device */
main {
  width: 100%;
}

article, aside {
  width: 100%;
  padding: 2em;
}

Here’s the result in all browsers — even very old ones that don’t support media queries:

Example screenshot without media query support.
Example screenshot without media query support.

When media queries are supported and the screen exceeds a specific width, say 500px, the <article> and <aside> elements can be positioned horizontally. This example uses a CSS grid, where the primary content uses approximately two-thirds of the width, and secondary content uses the remaining one-third:

/* larger device */
@media (min-width: 500px) {
  main {
    display: grid;
    grid-template-columns: 2fr 1fr;
    gap: 2em;
  }

  article, aside {
    width: auto;
    padding: 0;
  }
}

Here’s the result on larger screens:

Example screenshot with javascript media query support
Example screenshot with media query support.

Media Query Alternatives

Responsive designs can also be implemented in modern CSS using newer properties that intrinsically adapt the layout without examining the viewport dimensions. Options include:

  • calc, min-width, max-width, min-height, max-height, and the newer clamp property can all define dimensions that size elements according to known limits and the space available.
  • The viewport units vw, vh, vmin, and vmax can size elements according to screen dimension fractions.
  • Text can be shown in CSS columns which appear or disappear as the space allows.
  • Elements can be sized according to their child element sizes using min-content, fit-content, and max-content dimensions.
  • CSS flexbox can wrap — or not wrap — elements as they begin to exceed the available space.
  • CSS grid elements can be sized with proportional fraction fr units. The repeat CSS function can be used in conjunction with minmax, auto-fit, and auto-fill to allocate available space.
  • The new and (currently) experimental CSS container queries can react to the partial space available to a component within a layout.

These options are beyond the scope of this article, but they’re often more practical than cruder media queries, which can only respond to screen dimensions. If you can achieve a layout without media queries, it will probably use less code, be more efficient, and require less maintenance over time.

That said, there are situations where media queries remain the only viable layout option. They remain essential when you need to consider other screen factors such as aspect ratios, device orientation, color depth, pointer accuracy, or user preferences such as reduced animations and light/dark mode.

Do You Need Media Queries in JavaScript?

We’ve mostly talked about CSS up until now. That’s because most layout issues can — and should — be solved in CSS alone.

However, there are situations when it’s practical to use a JavaScript media query instead of CSS, such as when:

  • A component, such as a menu, has different functionality on small and large screens.
  • Switching to and from portrait/landscape affects the functionality of a web app.
  • A touch-based game has to change the <canvas> layout or adapt control buttons.
  • A web app adheres to user preferences such as dark/light mode, reduced animation, touch coarseness, etc.

The following sections demonstrate three methods that use media queries — or media-query-like options — in JavaScript. All the examples return a state string where:

  • small view = a screen with a width below 400 pixels;
  • medium view = a screen with a width between 400 and 799 pixels; and
  • large view = a screen with a width of 800 pixels or more.

Option 1: Monitor the Viewport Dimensions

This was the only option in the dark days before media queries were implemented. JavaScript would listen for browser “resize” events, analyse the viewport dimensions using window.innerWidth and window.innerHeight (or document.body.clientWidth and document.body.clientHeight in old IEs), and react accordingly.

This code outputs the calculated small, medium, or large string to the console:

const
  screen = {
    small: 0,
    medium: 400,
    large: 800
  };

// observe window resize
window.addEventListener('resize', resizeHandler);

// initial call
resizeHandler();

// calculate size
function resizeHandler() {

  // get window width
  const iw = window.innerWidth;
 
  // determine named size
  let size = null;
  for (let s in screen) {
    if (iw >= screen[s]) size = s;
  }

  console.log(size);
}

You can view a working demonstration here. (If using a desktop browser, open this link in a new window to make resizing easier. Mobile users can rotate the device.)

The above example examines the viewport size as the browser is resized; determines whether it’s small, medium, or large; and sets that as a class on the body element, which changes the background color.

The advantages of this method include:

  • It works in every browser that can run JavaScript — even ancient applications.
  • You’re capturing the exact dimensions and can react accordingly.

The disadvantages:

  • It’s an old technique that requires considerable code.
  • Is it too exact? Do you really need to know when the width is 966px versus 967px?
  • You may need to manually match dimensions to a corresponding CSS media query.
  • Users can resize the browser rapidly, causing the handler function to run again each time. This can overload older and slower browsers by throttling the event. It can only be triggered once every 500 milliseconds.

In summary, do not monitor viewport dimensions unless you have very specific and complex sizing requirements.

Option 2: Define and Monitor a CSS Custom Property (Variable)

This is a slightly unusual technique that changes the value of a custom property string in CSS when a media query is triggered. Custom properties are supported in all modern browsers (but not IE).

In the example below, the --screen custom property is set to “small”, “medium”, or “large” within an @media code block:

body {
  --screen: "small";
  background-color: #cff;
  text-align: center;
}

@media (min-width: 400px) {
 
  body {
    --screen: "medium";
    background-color: #fcf;
  }
 
}

@media (min-width: 800px) {
 
  body {
    --screen: "large";
    background-color: #ffc;
  }
 
}

The value can be output in CSS alone using a pseudo-element (but note that it must be contained within single or double quotes):

p::before {
  content: var(--screen);
}

You can fetch the custom property value using JavaScript:

const screen = getComputedStyle(window.body)
                 .getPropertyValue('--screen');

This is not quite the whole story, though, because the returned value contains all the whitespace and quote characters defined after the colon in the CSS. The string will be ‘ “large”‘, so a little tidying is necessary:

// returns small, medium, or large in a string
const screen = getComputedStyle(window.body)
                 .getPropertyValue('--screen')
                 .replace(/\W/g, '');

You can view a working demonstration here. (If using a desktop browser, open this link in a new window to make resizing easier. Mobile users can rotate the device.)

The example examines the CSS value every two seconds. It requires a little JavaScript code, but it’s necessary to poll for changes — you cannot automatically detect that the custom property value has changed using CSS.

Neither is it possible to write the value to a pseudo-element and detect the change using a DOM Mutation Observer. Pseudo-elements are not a “real” part of the DOM!

The advantages:

  • It’s a simple technique that mostly uses CSS and matches real media queries.
  • Any other CSS properties can be modified at the same time.
  • There’s no need to duplicate or parse JavaScript media query strings.

The primary disadvantage is you cannot automatically react to a change in browser viewport dimension. If the user rotates their phone from portrait to landscape orientation, the JavaScript would never know. You can frequently poll for changes, but that’s inefficient and results in the time lag you see in our demonstration.

Monitoring CSS custom properties is a novel technique, but it’s only practical when:

  1. The layout can be fixed at the point a page is initially rendered. A kiosk or point-of-sale terminal is a possibility, but those are likely to have fixed resolutions and a single layout, so JavaScript media queries become irrelevant.
  2. The site or app already runs frequent time-based functions, such as a game animation. The custom property could be checked at the same time to determine whether layout changes are required.

Option 3: Use the matchMedia API

The matchMedia API is slightly unusual but it allows you to implement a JavaScript media query. It’s supported in most browsers from IE10 upward. The constructor returns a MediaQueryList object that has a matches property which evaluates to true or false for its specific media query.

The following code outputs true when the browser viewport width is 800px or greater:

const mqLarge  = window.matchMedia( '(min-width: 800px)' );
console.log( mqLarge.matches );

A “change” event can be applied to the MediaQueryList object. This is triggered every time the state of the matches property changes: It becomes true (over 800px) after previously being false (under 800px) or vice versa.

The receiving handler function is passed the MediaQueryList object as the first parameter:

const mqLarge  = window.matchMedia( '(min-width: 800px)' );
mqLarge.addEventListener('change', mqHandler);

// media query handler function
function mqHandler(e) {
 
  console.log(
    e.matches ? 'large' : 'not large'
  );
 
}

The handler only runs when the matches property changes. It won’t run when the page is initially loaded, so you can call the function directly to determine the starting state:

// initial state
mqHandler(mqLarge);

The API works well when you’re moving between two distinct states. To analyze three or more states, such as small, medium, and large, it’ll require more code.

Start by defining a screen state object with associated matchMedia objects:

const
  screen = {
    small : null,
    medium: window.matchMedia( '(min-width: 400px)' ),
    large : window.matchMedia( '(min-width: 800px)' )
  };

It’s not necessary to define a matchMedia object on the small state because the medium event handler will trigger when moving between small and medium.

Event listeners can then be set for the medium and large events. These call the same mqHandler() handler function:

// media query change events
for (let [scr, mq] of Object.entries(screen)) {
  if (mq) mq.addEventListener('change', mqHandler);
}

The handler function must check all MediaQueryList objects to determine whether small, medium, or large is currently active. Matches must be run in size order since a width of 999px would match both medium and large — only the largest should “win”:

// media query handler function
function mqHandler() {
 
  let size = null;
  for (let [scr, mq] of Object.entries(screen)) {
    if (!mq || mq.matches) size = scr;
  }
 
  console.log(size);
 
}

You can view a working demonstration here. (If using a desktop browser, open this link in a new window to make resizing easier. Mobile users can rotate the device.)

The example uses are:

  1. Media queries in CSS to set and display a custom property (as shown in option 2 above).
  2. Identical media queries in matchMedia objects to monitor dimension changes in JavaScript. The JavaScript output will change at exactly the same time.

The main advantages of using the matchMedia API are:

  • It’s event-driven and efficient at processing media query changes.
  • It uses identical media query strings as CSS.

The disadvantages:

  • Handling two or more media queries requires more thought and code logic.
  • You probably need to duplicate media query strings in both CSS and JavaScript code. This could lead to errors if you don’t keep them in sync.

To avoid media query mismatches, you could consider using design tokens in your build system. Media query strings are defined in a JSON (or similar) file and the values are slotted into the CSS and JavaScript code at build time.

In summary, the matchMedia API is likely to be the most efficient and practical way to implement a JavaScript media query. It has some quirks, but it’s the best option in most situations.

Summary

Intrinsic CSS sizing options are increasingly viable, but media queries remain the basis of responsive web design for most sites. They will always be necessary to handle more complex layouts and user preferences, such as light/dark mode.

Try to keep media queries to CSS alone where possible. When you have no choice but to venture into the realm of JavaScript, the matchMedia API provides additional control for JavaScript media query components, which require additional dimension-based functionality.

Do you have any other tips for implementing a JavaScript media query? Share them in the comments section!

Craig Buckler

Freelance UK web developer, writer, and speaker. Has been around a long time and rants about standards and performance.