Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

CSS  Module 12 – Responsive design: media and container queries

Media queries: range syntax and user preferences

Write media queries with range syntax and, or and not, rem breakpoints, hover and pointer, and the motion, colour scheme, contrast and forced-colour queries.

  • Intermediate
  • 25 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Write media queries with the range syntax and logical operators
  • Detect input capabilities with the hover and pointer features
  • Respond to user preferences for reduced motion, colour scheme, contrast and forced colours

Before you start

What you’ll learn · Before you start

What you will learn

  • Write media queries with the range syntax and logical operators
  • Detect input capabilities with the hover and pointer features
  • Respond to user preferences for reduced motion, colour scheme, contrast and forced colours

Before you start

On this page

A media query asks the browser a question about where the page is shown, and applies its rules only when the answer is yes. @media (width >= 40rem) { … } asks whether the viewport is at least 40rem wide; @media (hover: hover) whether the main pointer can hover; @media (prefers-reduced-motion: reduce) whether the reader asked their system for less motion. Media queries ask about the viewport, the input and the reader’s settings, never about the box being styled: a component that must fit wherever it is placed needs container queries, the next lesson. This lesson covers how a query is read, the range syntax and the gap it closes, how em and rem behave inside queries, the hover and pointer features and the preference features, with every result measured in Chromium 153, Firefox 155 and WebKit 26.6.

How a media query is read

A query has an optional media type (screen, print or all) and conditions in parentheses. and needs every condition, or needs one, and not turns a condition around; mixing and with or at one level needs parentheses to say which comes first. A comma between whole queries means “any of these”. Queries ask three families of questions:

A media query asks about the viewport, the input or the reader’s settings; its rules apply only when the answer is true.@media <condition> { rules }Three families of questionstrue: the rules applyfalse or unknown: they don'tThe viewportwidth, height, orientation, resolutionfor page layoutThe inputhover, pointer, any-hover, any-pointerfor targets and hover effectsThe reader's settingsprefers-reduced-motion, prefers-color-scheme,prefers-contrast, forced-colorsto honour what the reader asked forasks one ofanswered with

What a media query can ask, and what the answer does

Text description of the diagram

At the top is a media query, written as @media followed by a condition and a block of rules. It asks one of three families of questions:

  1. The viewport: width, height, orientation and resolution, used for page layout.
  2. The input: hover, pointer, any-hover and any-pointer, used for target sizes and hover effects.
  3. The reader's settings: prefers-reduced-motion, prefers-color-scheme, prefers-contrast and forced-colors, used to honour what the reader asked for.

The answer is true, false or unknown. The rules apply only when it is true; a false or unknown answer, such as a misspelt feature, leaves them out.

The fastest way to learn the rules is to test queries with matchMedia(), which evaluates them as @media does. This page tests fourteen in a window 800 px wide, at the default text size, so 40rem is 640 px and 50rem is 800 px:

Fourteen queries, tested with matchMedia() HTML · queries.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Which media queries match</title>
  <style>:root { --wide: 40rem; }</style>
</head>
<body>
  <script>
    // Each query is tested with matchMedia(), which evaluates it the way @media does. The lesson's table gives the
    // answers in a window 800 px wide at the default text size, where 40rem is 640 px and 50rem is 800 px.
    const QUERIES = [
      '(width >= 40rem)',
      '(min-width: 40rem)',
      '(width < 50rem)',
      '(width <= 50rem)',
      '(30em <= width < 50em)',
      'screen and (width >= 40rem)',
      'print, (width >= 60rem)',
      'not print',
      'not (hover: hover)',
      '(widht >= 1px)',
      'not (widht >= 1px)',
      '(width >= 40rem) or (widht >= 1px)',
      '(width >= var(--wide))',
      '(width >= calc(30rem + 10px))',
    ];
    for (const q of QUERIES) {
      const m = matchMedia(q);
      console.log(`${q.padEnd(36)} ${m.matches}`);
    }
  </script>
</body>
</html>

All three engines gave the same answers:

Query Matches at 800 px Why
(width >= 40rem), (min-width: 40rem) yes the same question, written two ways
(width < 50rem) no 800 px is not less than 800 px
(width <= 50rem) yes <= includes the boundary
(30em <= width < 50em) no the range stops just before 800 px
screen and (width >= 40rem) yes the page is on a screen
print, (width >= 60rem) no neither query in the list is true
not print yes not turns the whole query around
(widht >= 1px) no a misspelt feature is unknown
not (widht >= 1px) no the opposite of unknown is still unknown
(width >= 40rem) or (widht >= 1px) yes or needs only one true condition
(width >= var(--wide)) no queries can’t read custom properties
(width >= calc(30rem + 10px)) yes calc() works in values

The unknown rows matter most. A typo doesn’t break the stylesheet: Media Queries Level 4 makes the condition unknown, and an unknown query never applies, even with not in front. The keyword only before a media type exists only for browsers from before media queries, and changes nothing today.

The range syntax, and the gap it closes

The range syntax compares a feature with ordinary operators: (width >= 40rem) means exactly what (min-width: 40rem) means, and (width <= 64rem) what (max-width: 64rem) means. It also says what the prefixes can’t: a strict comparison, (width < 64rem), and a range in one condition, (40rem <= width < 64rem). It is Baseline widely available: Chrome and Edge 104, Firefox 102 and Safari 16.4 support it.

The strict comparison fixes a real bug. A pair of breakpoints written with prefixes, (max-width: 959px) for the narrow layout and (min-width: 960px) for the wide one, leaves out every width between 959 and 960 px, and such widths exist. At 125 % display scaling, which Windows offers, one device pixel is 0.8 CSS px, so the layout width moves in steps of 0.8 px:

Two ways to split layouts at 960 px HTML · gap.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>A gap between two breakpoints</title>
</head>
<body>
  <script>
    // The layout width can be a fraction of a CSS pixel: at 125 % display scaling it moves in steps of 0.8 px.
    const width = Number(document.documentElement.getBoundingClientRect().width.toFixed(2));
    const pairs = {
      'min/max': ['(max-width: 959px)', '(min-width: 960px)'],
      'range': ['(width < 960px)', '(width >= 960px)'],
    };
    for (const [syntax, [narrow, wide]] of Object.entries(pairs)) {
      const n = matchMedia(narrow).matches;
      const w = matchMedia(wide).matches;
      console.log(`${width} px · ${syntax}: narrow ${n}, wide ${w}${!n && !w ? ' · neither layout applies' : ''}`);
    }
  </script>
</body>
</html>

With Chromium and Firefox set to 125 % scaling and a window of 959 CSS px, the page measured 959.2 px wide. Both prefixed queries were false, so neither layout applied and the page lost its styles at that width. The range pair, (width < 960px) and (width >= 960px), always gives exactly one match. The same check, for the widths that 125 %, 150 % and 300 % scaling produce:

Which layout each width gets JavaScript · programs/gap.mjs
// Two layouts split at 960 px, written three ways. Screens scaled to 125 %, 150 % or 300 % give layout widths in
// steps of 0.8, 0.67 or 0.33 CSS px, so a width such as 959.2 px is real.
const SPLITS = {
  'max-width: 959px / min-width: 960px': [(w) => w <= 959, (w) => w >= 960],
  'max-width: 959.98px / min-width: 960px': [(w) => w <= 959.98, (w) => w >= 960],
  'width < 960px / width >= 960px': [(w) => w < 960, (w) => w >= 960],
};

const widths = [959, 959.2, 1439 / 1.5, 2879 / 3, 960];
console.log(('width       ' + Object.keys(SPLITS).map((k, i) => `split ${i + 1}`.padEnd(10)).join('')).trimEnd());
for (const w of widths) {
  const cells = Object.values(SPLITS).map(([narrow, wide]) => {
    const n = narrow(w);
    const x = wide(w);
    return (n && x ? 'both' : n ? 'narrow' : x ? 'wide' : 'NEITHER').padEnd(10);
  });
  console.log(`${w.toFixed(2).padEnd(12)}${cells.join('')}`.trimEnd());
}
Object.keys(SPLITS).forEach((k, i) => {
  const [narrow, wide] = k.split(' / ');
  console.log(`split ${i + 1}: narrow layout in (${narrow}), wide layout in (${wide})`);
});

Output

width       split 1   split 2   split 3
959.00      narrow    narrow    narrow
959.20      NEITHER   narrow    narrow
959.33      NEITHER   narrow    narrow
959.67      NEITHER   narrow    narrow
960.00      wide      wide      wide
split 1: narrow layout in (max-width: 959px), wide layout in (min-width: 960px)
split 2: narrow layout in (max-width: 959.98px), wide layout in (min-width: 960px)
split 3: narrow layout in (width < 960px), wide layout in (width >= 960px)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node gap.mjs

Bootstrap 5.3 subtracts 0.02 px from its max-width breakpoints for the same reason; a width would then have to fall between 959.98 and 960 px to slip through, which no common scaling produces. With the range syntax you don’t need the trick.

em and rem in media queries

Inside a media query, em and rem don’t come from the page’s styles. Media Queries Level 4 bases them on the initial font size: the browser’s default of 16 px, or the size the reader chose in the browser’s settings. This page sets html { font-size: 62.5% }, the old trick that makes 1rem 10 px in its CSS, and tests three queries:

rem and em in media queries HTML · rem-breakpoints.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>rem and em in media queries</title>
  <style>
    html { font-size: 62.5%; } /* the old trick that makes 1rem 10px inside the page */
  </style>
</head>
<body>
  <script>
    // In a media query, rem and em use the browser's initial font size (16 px unless the reader changed it), never
    // the font-size the page sets on html.
    const root = getComputedStyle(document.documentElement).fontSize;
    for (const q of ['(width >= 40rem)', '(width >= 40em)', '(width >= 640px)']) {
      console.log(`html font-size ${root} · ${q} ${matchMedia(q).matches}`);
    }
  </script>
</body>
</html>
Window, default text (width >= 40rem) (width >= 640px)
600 px, 16 px no no
700 px, 16 px yes yes
700 px, 20 px no yes

(width >= 40em) gave the same answer as 40rem in every row, and the page’s own root font size was 10 px with the 16 px default and 12.5 px with 20 px. The first row shows that the page’s 10 px root changed nothing: 40rem in the query stayed 640 px. The last row shows what a reader’s setting does: with 20 px text, 40rem became 800 px, and the narrow layout stayed until the window was that wide. All three engines agreed at the default size; Chromium and Firefox, whose default text size the test could change, agreed at 20 px. That is the reason to write breakpoints in rem or em: the layout switches when the text no longer fits, whatever size the reader picked. A px breakpoint ignores that setting, so large text gets squeezed into the wide layout. Browser zoom is different: it shrinks the width in CSS px, so px and rem breakpoints react to it in the same way.

Hover and pointer: what the input can do

hover says whether the main pointing device can hover (hover or none), and pointer how precise it is (fine, coarse or none). any-hover and any-pointer ask the same about every input, so a laptop with a touchscreen typically has pointer: fine for its trackpad and also any-pointer: coarse for the screen. Neither feature can detect a keyboard. This product card reveals its Save button on hover only where hover is reliable, and keeps it visible everywhere else:

A Save button that hides only where hover exists

hover-reveal.css

/* The Save button is always visible, so every device can use it. It hides until the card is hovered, or holds
   focus, only when the main pointer can hover precisely and no input is coarse: a laptop with a touchscreen keeps
   it visible for the finger. */
.save { opacity: 1; min-block-size: 24px; min-inline-size: 24px; }

@media (hover: hover) and (pointer: fine) and (not (any-pointer: coarse)) {
  .product .save { opacity: 0; transition: opacity 150ms; }
  .product:hover .save,
  .product:focus-within .save { opacity: 1; min-block-size: 24px; min-inline-size: 24px; }
}

/* Targets of 44 px wherever any input is coarse; 24 px is the minimum everywhere else. */
@media (any-pointer: coarse) {
  .save { min-block-size: 44px; min-inline-size: 44px; }
}

hover-reveal.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Hover only where hover exists</title>
  <link rel="stylesheet" href="hover-reveal.css">
</head>
<body>
  <div class="product">
    <h2>Cotton kurta</h2>
    <button class="save" type="button">Save</button>
  </div>
  <script>
    const features = ['(hover: hover)', '(pointer: fine)', '(pointer: coarse)', '(any-hover: hover)', '(any-pointer: coarse)'];
    console.log(features.map((q) => `${q} ${matchMedia(q).matches}`).join(' · '));
    const save = document.querySelector('.save');
    const style = getComputedStyle(save);
    console.log(`Save button: opacity ${style.opacity}, ${Math.round(save.getBoundingClientRect().height)} px tall`);
  </script>
</body>
</html>
Emulated input Features that matched Save button
mouse, 1280 px hover: hover, pointer: fine hidden until hover or focus, 24 px tall
touch, 390 px phone hover: none, pointer: coarse, any-pointer: coarse visible, 44 px tall
touch, 1280 px hover: none, pointer: coarse, any-pointer: coarse visible, 44 px tall

The three engines matched in every row. The same features answered both touch rows, which is the point: layout follows the width, while hover and pointer describe the input, so a large touchscreen is treated as touch. Three rules come from this:

  • Never let a task depend on hover. A finger can’t hover, and touch browsers apply hover styles only to what was tapped.
  • Reveal with focus too. :focus-within shows the button to keyboard users who tab into the card.
  • Size targets for the coarsest input. WCAG 2.2 asks for targets of at least 24 by 24 CSS px (success criterion 2.5.8) and 44 by 44 at level AAA (2.5.5); any-pointer: coarse applies the larger size when any input is a finger.

User preferences

Four preference features are Baseline widely available, and each asks one question:

  • prefers-reduced-motion: reduce or no-preference. Someone with a vestibular disorder can be made dizzy by movement they didn’t start.
  • prefers-color-scheme: light or dark, from the system or the browser.
  • prefers-contrast: more, less, custom or no-preference.
  • forced-colors: active when the browser replaces the page’s colours with the reader’s own palette, as Windows contrast themes do.

The stylesheet below gives each one a small, separate job: motion is added only for people who haven’t asked for less, a dark palette swaps custom properties, more contrast strengthens edges, and forced colours keep a border:

Four preferences, four small blocks

preferences.css

/* Four user preferences. Each block changes only what the preference is about. */
:root {
  color-scheme: light dark;
  --surface: #ffffff;
  --text: #1f2933;
  --edge: #c3c9d1;
}

body { background: var(--surface); color: var(--text); }
.card { border: 1px solid var(--edge); border-radius: 8px; box-shadow: 0 2px 6px #0003; }

/* Motion is added for people who have not asked their system for less of it. */
@media (prefers-reduced-motion: no-preference) {
  .card { transition: translate 250ms ease-out; }
  .card:hover { translate: 0 -4px; }
}

/* A dark palette when the system or browser asks for one. */
@media (prefers-color-scheme: dark) {
  :root { --surface: #14171c; --text: #e4e7ec; --edge: #48505c; }
}

/* A stronger edge when the reader asks for more contrast. */
@media (prefers-contrast: more) {
  :root { --edge: currentColor; }
}

/* Forced colours replace the palette with the reader's own and drop shadows, so the border carries the edge. */
@media (forced-colors: active) {
  .card { border-color: CanvasText; }
}

preferences.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>User preference queries</title>
  <link rel="stylesheet" href="preferences.css">
</head>
<body>
  <div class="card">Order #40211 is out for delivery.</div>
  <script>
    const card = getComputedStyle(document.querySelector('.card'));
    const body = getComputedStyle(document.body);
    const queries = ['(prefers-reduced-motion: reduce)', '(prefers-color-scheme: dark)', '(prefers-contrast: more)', '(forced-colors: active)'];
    console.log(queries.filter((q) => matchMedia(q).matches).join(' ') || 'no preference query matches');
    console.log(`background ${body.backgroundColor} · border ${card.borderTopColor} · shadow ${card.boxShadow} · transition ${card.transitionDuration}`);
  </script>
</body>
</html>

Loaded with Playwright’s media emulation for each preference in turn:

Emulated What changed
nothing white page, grey border, soft shadow, a 0.25 s transition
reduced motion the transition became 0 s
dark scheme a near-black page with a dark grey border
more contrast the border took the text colour
forced colours a black border; Chromium and Firefox removed the shadow

In forced colours mode Chromium and Firefox removed the shadow, which is why the card’s edge has to be a border. WebKit matched the query under emulation but kept the shadow and the page’s own colours. Two more features are not ready: prefers-reduced-transparency works only in Chromium, and prefers-reduced-data in no browser, so use them only to add something extra.

Keeping breakpoints organised

Media queries can’t read custom properties, as the var() row above showed, and @custom-media, the planned way to name a query, hasn’t shipped in any browser. Until it does, keep the values in one documented list, and nest each component’s queries inside the component’s rule, which CSS nesting allows in every current engine:

Breakpoints kept with their component CSS · breakpoints.css
/* The site's breakpoints, listed in one place. Media queries can't read custom properties, and @custom-media has
   not shipped in any browser yet, so each value is written out where it is used:
     two columns     33rem   (528 px at the default text size)
     three columns   49rem   (784 px)
   Search the code base for "33rem" and "49rem" before changing one. */
.products {
  display: grid;
  gap: 1rem;

  /* Nested queries keep a component's breakpoints next to the component. */
  @media (width >= 33rem) {
    grid-template-columns: repeat(2, 1fr);
  }

  @media (width >= 49rem) {
    grid-template-columns: repeat(3, 1fr);
  }
}

All three engines applied the nested queries, giving one, two and three columns at 400, 600 and 900 px. In JavaScript, matchMedia(query).matches gives the same answer as @media, and a change event fires when it flips, which is how scripts follow a breakpoint or a preference without polling the window size.

CSS Toolkit Its Media queries tab builds @media rules in classic or range syntax and says whether your window matches them.

Practise: evaluate width queries

Exercise · Medium · JavaScript

Evaluate width media queries the way a browser does

Write matchesWidth(query, { width, fontSize = 16 }) in match.mjs and export it. It answers whether a media query about the viewport's width matches a viewport width CSS px wide, in a browser whose initial font size is fontSize px. It returns true or false.

A query is one or more conditions joined by and , each in parentheses:

  • (min-width: 40rem) and (max-width: 59.99em), which include their value;
  • the range syntax with width on either side: (width >= 40rem), (width < 50em), (640px <= width);
  • a range with two values: (30em <= width < 50em).

The operators are <, <=, > and >=, and values are a number with px, em or rem. In media queries em and rem are both the initial font size, never the font-size a page sets on html, so with fontSize: 20, 40rem is 800 px. The query matches only when every condition does.

Widths may be fractions of a pixel, as they are at 125 % display scaling: at 959.2 px, (max-width: 959px) and (min-width: 960px) are both false.

The sample tests import matchesWidth from match.mjs and run in your browser.

Starter code · match.mjs

/** Whether a width media query matches a viewport `width` px wide, with an initial font size of `fontSize` px. */
export function matchesWidth(query, { width, fontSize = 16 }) {
  // Replace this line with your code.
  return false;
}
The sample tests · match.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { matchesWidth } from './match.mjs';

test('min-width and max-width include their value', () => {
  assert.equal(matchesWidth('(min-width: 40rem)', { width: 640 }), true);
  assert.equal(matchesWidth('(min-width: 40rem)', { width: 639.5 }), false);
  assert.equal(matchesWidth('(max-width: 40em)', { width: 640 }), true);
  assert.equal(matchesWidth('(max-width: 40em)', { width: 640.5 }), false);
});

test('the range operators, at their boundaries', () => {
  assert.equal(matchesWidth('(width >= 40rem)', { width: 640 }), true);
  assert.equal(matchesWidth('(width > 40rem)', { width: 640 }), false);
  assert.equal(matchesWidth('(width < 50rem)', { width: 800 }), false);
  assert.equal(matchesWidth('(width <= 50rem)', { width: 800 }), true);
});

test('the value can come first', () => {
  assert.equal(matchesWidth('(40rem > width)', { width: 600 }), true);
  assert.equal(matchesWidth('(640px <= width)', { width: 600 }), false);
});

test('a range with two values', () => {
  assert.equal(matchesWidth('(30em <= width < 50em)', { width: 480 }), true);
  assert.equal(matchesWidth('(30em <= width < 50em)', { width: 800 }), false);
  assert.equal(matchesWidth('(30em <= width < 50em)', { width: 479 }), false);
});

test('em and rem follow the initial font size', () => {
  assert.equal(matchesWidth('(width >= 40rem)', { width: 700, fontSize: 20 }), false);
  assert.equal(matchesWidth('(width >= 640px)', { width: 700, fontSize: 20 }), true);
  assert.equal(matchesWidth('(min-width: 35em)', { width: 700, fontSize: 20 }), true);
});

test('every condition joined by and must match', () => {
  assert.equal(matchesWidth('(min-width: 30rem) and (max-width: 47.99rem)', { width: 767 }), true);
  assert.equal(matchesWidth('(min-width: 30rem) and (max-width: 47.99rem)', { width: 768 }), false);
  assert.equal(matchesWidth('(min-width: 30rem) and (max-width: 47.99rem)', { width: 400 }), false);
});

test('fractional widths fall between min- and max- breakpoints', () => {
  assert.equal(matchesWidth('(max-width: 959px)', { width: 959.2 }), false);
  assert.equal(matchesWidth('(min-width: 960px)', { width: 959.2 }), false);
  assert.equal(matchesWidth('(width < 960px)', { width: 959.2 }), true);
});
A hint

Split the query on and , take the text inside each pair of parentheses, and turn every value into px first (em and rem multiply by fontSize). For a range, split on the operators with a regular expression that keeps them, such as /\s*(<=|>=|<|>)\s*/, put width in place of the word, and check each neighbouring pair.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 A window is exactly 800 px wide and the browser uses the default text size. Does @media (width < 50rem) match?

    Choose one answer.

    Show the answer to question 1

    Answer: No, because 50rem is 800 px and < leaves out the boundary

    In a media query 50rem is 50 × 16 = 800 px, and width < 800px is false at exactly 800 px. (width <= 50rem) matches, and so does (max-width: 50rem), which includes its value. All three engines agreed in the lesson's run.

  2. Question 2 of 6 A page sets html { font-size: 62.5% }, so 1rem is 10 px in its CSS. In a window 600 px wide, with the default text size, does @media (width >= 40rem) match?

    Choose one answer.

    Show the answer to question 2

    Answer: No, because in a media query 40rem is 640 px

    Relative units in media queries use the initial font size, the browser's default or the reader's own setting, never the page's styles. 40rem is 640 px here, and 800 px for a reader who chose 20 px text. The page's 10 px root changes nothing in its queries.

  3. Question 3 of 6 Which of these queries match on a phone whose only input is its touchscreen?

    Choose every answer that is right.

    Show the answer to question 3

    Answer:

    • (hover: none)
    • (pointer: coarse)
    • (any-pointer: coarse)

    A finger can't hover and isn't precise, so the phone matches hover: none and pointer: coarse, and with only one input the any- features say the same. Under touch emulation all three engines reported exactly that in the lesson's run, and the Save button stayed visible and grew to 44 px.

  4. Question 4 of 6 Which way of writing an animation respects readers who asked their system for less motion?

    Choose one answer.

    Show the answer to question 4

    Answer: Add the animation inside @media (prefers-reduced-motion: no-preference)

    Written inside no-preference, the motion exists only for people who have not asked for less, and a browser that doesn't know the feature shows no animation. In the lesson's run the card's transition was 0.25 s by default and 0 s under reduced-motion emulation.

  5. Question 5 of 6 A stylesheet contains @media (widht >= 40rem) { … } with a typo. What happens?

    Choose one answer.

    Show the answer to question 5

    Answer: The query never matches, and the rest of the stylesheet still works

    An unknown feature makes the condition unknown, which counts as false, so the rules inside never apply. Even not (widht >= 40rem) stayed false in all three engines, because the negation of unknown is still unknown. Nothing else in the stylesheet is affected.

  6. Question 6 of 6 At 125 % display scaling, one CSS px is 1.25 device pixels, and windows are whole device pixels wide. In steps of how many CSS px can the layout width change?

    Type a number.

    Show the answer to question 6

    Answer: 0.8 px (anything from 0.799 to 0.801 counts)

    One device pixel is 1 ÷ 1.25 = 0.8 CSS px, so widths such as 959.2 px occur. In Chromium and Firefox at that scale, a window measured 959.2 px, where (max-width: 959px) and (min-width: 960px) were both false.

Interview questions

Fresher · What is the difference between (min-width: 48em) and (width >= 48em), and why write breakpoints in em or rem rather than px?

They match the same widths: Media Queries Level 4 defines the min- prefix as the >= comparison, so both mean that the viewport is at least 48em wide. The range form also says what the prefixes can’t: a strict comparison such as (width < 48em), and a range in one condition, (30em <= width < 48em), which leaves no gap at fractional widths. In media queries, em and rem both use the browser’s initial font size, 16 px unless the reader changed it, never the font-size a page sets on html, so 48em is 768 px by default and 960 px for someone who chose 20 px text. That is the reason to prefer them: the layout switches when the text no longer fits, whatever size the reader picked.

Key takeaways

  • and needs every condition, or one, not reverses one; a comma means “any of these queries”.
  • A misspelt or unsupported feature is unknown, and an unknown query never applies, even with not in front.
  • (width >= 40rem) equals (min-width: 40rem); use < for the upper end so fractional widths such as 959.2 px always get a layout.
  • In media queries, em and rem are the reader’s default text size, never the page’s root font size.
  • hover and pointer describe the main input, any-hover and any-pointer every input; never hide a task behind hover.
  • Add motion inside (prefers-reduced-motion: no-preference), swap custom properties for the dark scheme, and draw edges with borders, which survive forced colours.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.