Media Queries Beyond Width
Width is one axis. Colour scheme, reduced motion, contrast, pointer precision, hover capability and orientation are separate questions — and the preference ones are accessibility features, not theming.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What can I actually ask the browser about the user's conditions, and which of those answers am I obliged to respect?
A person has already told their operating system what they need: less motion because it makes them ill, more contrast because they cannot resolve grey on grey, a dark interface because the room is dark. They expect the web to have heard.
Media queries are for widths. Everything else — dark mode, touch, animation — is detected in JavaScript from the user agent string and configured again inside our own settings screen.
The user already answered these questions at the OS level. Asking again is not a feature; it is a form that a person with a vestibular disorder has to fill in on every site they visit.
- The user already answered these questions at the OS level. Asking again is not a feature; it is a form that a person with a vestibular disorder has to fill in on every site they visit.
- A user-agent string names a product line, not an input device. A tablet with a keyboard case, a laptop with a touchscreen and a console browser all report something the sniffing logic did not anticipate.
- Preferences change while the page is open. Someone switches to dark at sunset, or turns on reduce-motion because your carousel has just made them queasy. A value read once at load never updates (Long-Lived Clients and Version Skew).
- Width used as a proxy for touch is wrong in both directions: a desktop user at high zoom gets a touch layout on a mouse-driven machine, and a tablet in landscape gets mouse-sized targets (The Viewport and Device Pixels).
- An animation that only respects reduced motion once JavaScript has loaded still plays the first, largest transition — the one before hydration, which is precisely the one that hurts (Hydration).
What is actually happening
In the browser, not in the framework.
- A media query is a boolean expression over media features, evaluated by the browser and re-evaluated whenever an input changes. Rules inside a non-matching query are parsed and held in the CSSOM but contribute nothing to the cascade (The Cascade).
- Viewport features —
width,height,orientation,aspect-ratio,resolution— describe the space. Modern range syntax writes these as comparisons:@media (width >= 40rem). - Interaction features —
pointer,any-pointer,hover,any-hover— describe input capability.pointerreports the *primary* pointing device (fine,coarseornone);any-pointerreports whether *any* attached device matches. Two different questions, and confusing them is the classic bug (Pointer Events). - User preference features —
prefers-color-scheme,prefers-reduced-motion,prefers-reduced-transparency,prefers-contrast,forced-colors— surface a setting the user chose in their OS or browser. They are the browser relaying an explicit request, not a guess. - The browser evaluates all of these itself, so matching rules apply during the first style calculation — before any script runs. That is the structural advantage over JavaScript detection, and the reason a CSS-level answer is never late (Style Calculation).
window.matchMedia(query)exposes the same evaluation to script, and itschangeevent fires when the answer flips. That is how you *react* to a preference change rather than sampling it once.
What this makes the browser do
And which of it is avoidable.
- Evaluating a media list is trivial; it happens on viewport change, on a preference change and on a display change. The work is not the evaluation but the restyle it triggers.
- A preference flip that changes colours invalidates style for everything affected and repaints it. If the colours live in custom properties on
:root, that is one inherited-property change propagating down, which is cheap (Custom Properties). - A width change that crosses an arrangement breakpoint invalidates style *and* layout for the affected subtree, then paints. This is why arrangement breakpoints cost meaningfully more than colour ones (The Cost of a Change).
- Rules inside every non-matching query are still downloaded, parsed and held in memory. Media queries reduce what applies, never what ships (Minification Is Not Compression).
- A
matchMedialistener that runs script on every change adds main-thread work to an event the browser could have handled entirely in CSS. Prefer the stylesheet wherever the outcome is purely visual (What the Main Thread Owns).
Seven different questions
Grouping these under "media queries" hides the fact that they ask about entirely different things: the space, the input device, and the person. Width tells you nothing about whether someone is using a finger, and a coarse pointer tells you nothing about how much room you have.
The last column is the point. Each feature has a canonical misuse common enough to name, and in every case the misuse has the same shape: treating a specific capability answer as a general statement about "what kind of device this is".
| Feature | The question it answers | Changes when | The canonical misuse |
|---|---|---|---|
width / min-width | How much horizontal space does the viewport have? | Resize, rotate, split screen, browser zoom | Read as "which device", so a zoomed desktop user is treated as a phone |
prefers-color-scheme | Has the user asked for a dark or light interface? | OS or browser setting, sometimes on a sunset schedule | Sampled once in JavaScript at load and cached for the session |
prefers-reduced-motion | Has the user asked for less motion? | OS accessibility setting, possibly mid-session | Implemented as "disable every animation" rather than "remove large motion" |
prefers-contrast | Does the user want more or less contrast within your palette? | OS setting | Confused with forced-colors, which discards your palette entirely |
pointer / any-pointer | How precise is the primary pointer — or any attached pointer? | A device is attached, detached or paired | Replaced by user-agent sniffing, which describes a product line |
hover / any-hover | Can the primary pointer hover? | Same as above | Assumed to be the exact inverse of pointer: coarse |
orientation | Is the viewport wider than it is tall? | Rotation, or any window reshape | Used as a phone detector; almost every desktop window is landscape |
Preferences are requests, and the base rules are the safe ones
The ordering in the stylesheet is load-bearing. If the base rule animates and a reduce query removes it, then every browser that does not support the query and every user whose platform reports nothing gets the animation. If the base rule is still and a no-preference query adds motion, the failure mode is a missing transition — which harms nobody.
The same additive logic applies to colour. Define the light palette on bare :root, redefine tokens under the dark preference, and redefine them again under an explicit user override so the override wins in both directions. A colour whose only definition lives inside a media block has no value when that block does not match.
semantics A radio group — native input type="radio" inside a fieldset with a legend, or a labelled <select>. Three options, not a two-state toggle: "system" has to stay reachable, because it is the only setting that keeps tracking the user's OS.
| Tab | Moves focus into the group, landing on the currently selected option rather than the first one — native radio behaviour. |
| Arrow keys | Move between options within the group and select as they move, which is what a screen-reader user expects from a radio group. |
| Space / Enter | Activates, in a button-based implementation. With native radios, arrow keys have already selected and Enter submits any surrounding form. |
- — Changing the theme must not move focus. The control stays focused and the page restyles around it.
- — The focus ring must remain visible in every theme and under forced colours — a ring drawn in a brand colour disappears the moment the platform replaces your palette.
- — If the control lives in a menu that closes on selection, focus returns to the trigger that opened it (Focus Management).
- — The selected option is announced by the native radio semantics. No live region is needed, and adding one produces a double announcement.
- — Do not announce "theme changed": the state change is already carried by the control, and a live region for a purely visual change is noise (Live Regions and Announcement).
usually broken by Shipping only the toggle and ignoring prefers-color-scheme, so the user configures dark mode again on every site; or shipping only the query with no override, so a user whose OS is dark but who needs light for this particular content has nowhere to go. Both halves are required.
1/* Base: still, and themed light. Nothing here assumes support. */2:root {3 color-scheme: light dark; /* themes scrollbars, form controls, the canvas */4 --bg: #ffffff;5 --fg: #16181d;6 --border: #d7dbe0;7}8.panel { transition: none; }9 10/* Motion is ADDED only when the user has expressed no objection. */11@media (prefers-reduced-motion: no-preference) {12 .panel { transition: transform 200ms ease, opacity 200ms ease; }13}14 15/* System preference supplies the value; an explicit choice overrides it. */16@media (prefers-color-scheme: dark) {17 :root:not([data-theme="light"]) { --bg: #101215; --fg: #e8eaed; --border: #2a2f36; }18}19:root[data-theme="dark"] { --bg: #101215; --fg: #e8eaed; --border: #2a2f36; }20 21/* More contrast means a tighter palette, not an inversion. */22@media (prefers-contrast: more) {23 :root { --fg: #000000; --border: #000000; }24 :focus-visible { outline-width: 3px; }25}26 27/* Input capability, not screen size. */28@media (pointer: coarse) {29 .toolbar button { min-block-size: 2.75rem; min-inline-size: 2.75rem; }30}Read the motion block and the colour blocks as one idea in two places: the unqueried value must be the one that is safe when nothing matches. The :not([data-theme="light"]) guard is what stops the system preference from beating an explicit user choice — without it, a user who picked light gets dark at sunset.
Detecting touch without sniffing
Every row below is a real technique that shipped somewhere, and a real user it excluded. The pattern connecting them is substituting a proxy — a product name, an API's existence, a viewport width — for the capability actually being asked about.
The honest version is often not a branch at all. any-pointer: coarse says "this could be touched", and designing targets that work for a finger *and* a mouse is more robust than switching between two designs on a signal that can change mid-session.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| User-agent string contains "Mobile" | A tablet with a keyboard case gets phone-sized targets; a touchscreen laptop keeps hover-only menus | The UA string names a product line and a rendering engine, never the device currently attached | Query pointer and hover, which describe capability and update when hardware changes (Pointer Events). |
| Presence of touch events used as "is touch" | Desktop Chrome reports support; hover affordances vanish on ordinary laptops | Touch event support is compiled into the browser, not a statement about hardware | Feature-detecting an *API* answers a different question from detecting an *input*. Use the interaction media features. |
| Width used as a proxy for touch | A desktop user at 200% zoom gets the touch layout; a tablet in landscape gets the mouse one | Zoom changes viewport width in CSS pixels, and screen size and input are independent axes | Split them: width drives layout, pointer drives affordance size (Fluid Layout First). |
| Assuming one input for the whole session | A user pairs a mouse and the targets become oversized, or unpairs it and they are too small to hit | pointer reports the primary pointer, and it can change while the page is open | Prefer any-pointer: coarse as "could be touched" and design one set of targets that works for both (Accessible Component Patterns). |
| A disclosure that only opens on hover | The content is unreachable by touch, by keyboard and by voice control | Hover is a capability some users simply do not have | Every hover affordance needs a click and focus path; hover is an accelerator, never the mechanism (Keyboard Operability). |
How to build it
Most important first.
- Write the base rules as the safe, reduced, no-assumptions experience and let queries *add*.
@media (prefers-reduced-motion: no-preference)adds animation;@media (prefers-reduced-motion: reduce)removes it, which means every browser without support and every user without a reported preference keeps the motion. - Set
color-schemeon:rootso the browser themes form controls, scrollbars and the canvas to match. Without it, a dark page still gets a light scrollbar and a light<select>(What Native Elements Already Do). - Query capability, not identity.
pointer: coarseanswers "are these targets going to be hit with a finger"; a user-agent string does not. - Give the user an override *and* honour the system default. The correct shape is a three-state control — light, dark, system — where system is the default and the media query supplies its value (Persistent Client State).
- Keep the plumbing in custom properties so a single token swap re-themes everything, rather than repeating colours inside each query (Design Tokens).
- Use a
matchMediachange listener only where script must genuinely react — recomputing a canvas palette, re-rendering a chart. Anything expressible in CSS stays in CSS (Contrast, Colour and Motion).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- These queries *are* the accessibility feature.
prefers-reduced-motionexists because parallax and large transitions trigger nausea and vertigo in people with vestibular disorders; ignoring it is not a styling choice, it is causing symptoms. prefers-contrast: moreshould tighten your palette — darker text, visible borders, stronger focus rings — rather than merely inverting it. Underforced-colors, use the system colour keywords and confirm focus and selected state are still distinguishable once your palette is discarded (Contrast, Colour and Motion).pointer: coarseis the right hook for target size. WCAG 2.2 publishes a minimum target size and a touch-primary user is exactly who it protects; enforce it asmin-block-size/min-inline-sizeon the control itself so the hit area and the visible affordance stay the same shape.hover: nonemeans a hover-only disclosure is unreachable — but so is one for a keyboard user with a mouse attached. Every hover affordance needs a focus and activation path regardless of what the query reports (Keyboard Operability).- A theme control must be a real control: a
buttonor a radio group with an accessible name and a communicated current state, not adivwith a click handler — and it must not remove the user's ability to fall back to their system setting (Semantics Before ARIA).
What can go wrong
- Reduce-motion implemented as a blanket
animation: none !important. Some motion carries meaning — a spinner, a progress bar, a transition showing where a panel came from. Removing all of it can leave a user with no feedback at all; the requirement is to remove *large, unexpected* motion, not every change. - Detecting dark mode in JavaScript and applying a class, which paints the light theme first and then flips. The flash is worst for exactly the users who chose dark because bright light hurts.
- Confusing
prefers-contrastwithforced-colors. The first says the user wants more contrast within your palette; the second says the platform is replacing your palette entirely and your carefully chosen colours are simply not going to be used. - Treating
hover: noneas the exact inverse ofpointer: coarse. A device can have a coarse pointer that hovers, or a fine pointer that does not. Ask the question you actually mean. - The mitigation failing: a site that respects
prefers-reduced-motionin CSS while its hero animation is driven by a JavaScript animation library that never checks. The stylesheet is compliant and the page is not. - A preference read once into a module-scoped constant. Correct at load, silently stale for the rest of a session that may last days (The Seven Kinds of State).
- A preference can change between the server rendering HTML and the browser applying styles, so a theme baked into markup on the server can disagree with the client for one paint (Hydration Mismatch).
- A
matchMediachange event and the user's explicit override can arrive in either order. Decide which wins before it happens, or the theme depends on the timing of a sunset. - Across tabs, one tab writing a persisted theme choice while another reacts to the OS change leaves two tabs disagreeing until both settle (State Synchronization).
- Every one of these features is a fingerprinting surface. Colour scheme, contrast preference, reduced motion, pointer type and viewport size combine into a signal that identifies a browser far more precisely than any one of them suggests. Do not collect them into an analytics payload without a reason you would state out loud (Session Replay and the Privacy It Costs).
- The browser will not stop a third-party script from calling
matchMedia. Anything running in the page can read the same preferences, and a preference is information about a person, not about a device (Third-Party Scripts and the Supply Chain). - Nothing here is a security control. A layout or a feature selected by a media query says nothing about what the user may do; the server decides that, and the query is not evidence of anything (What the Frontend Is Responsible For in Auth).
- "
prefers-reduced-motionmeans no animation." It means no large, unexpected, vestibular-triggering motion. An opacity fade and a spinner are usually fine; a full-screen parallax slide is not. - "Dark mode is a theming feature." It is a preference the user set for a reason, often photophobia or a migraine trigger. Overriding it because it does not match the brand is not a neutral decision.
- "
pointer: coarsemeans mobile." It means the primary pointing device is imprecise. A television remote, a console controller and a kiosk touchscreen all match, and none of them is a phone. - "
orientation: landscapemeans a phone turned sideways." It means the viewport is wider than it is tall, which a desktop window almost always is. Used as a phone detector it applies phone rules to every desktop user. - "Media queries make the page smaller." They change which rules apply, not which bytes are downloaded. Everything inside every query ships (Bundle Analysis).
Measuring it, and what changes in the field
- DevTools can emulate
prefers-color-scheme,prefers-reduced-motion,prefers-contrastand forced colours from the rendering panel. That makes it a one-click check that belongs in every review (A Mental Model of the Devtools). - The device toolbar can emulate a coarse pointer, which is the only convenient way to exercise
pointer: coarserules from a laptop. - Automated accessibility runs check contrast under the default palette but not under
forced-colors, and they cannot judge whether the remaining motion is meaningful. Both need a human pass (Accessibility Testing). - In the field, record which preference branch a session took as a low-cardinality dimension. Learning that a measurable share of your users run reduce-motion changes the argument about the carousel (Real User Monitoring).
- Visual regression across the preference matrix — light, dark, high contrast, forced colours — is cheap to add and catches the token that was only ever defined in one branch (Visual Regression Testing).
- On a low-end device, a preference flip that restyles the whole document is a visible stall rather than an instant swap, because the restyle and repaint compete with whatever else is on the main thread (Long Tasks).
- On a slow network, a theme applied by a late stylesheet or a script produces a flash of the wrong theme measured in seconds rather than frames. Inline the tokens that decide the first paint (Render-Blocking Resources).
- In an in-app webview or an embedded browser, some preference features may not be reported at all. Treat "no preference" as the safe default — which is what the additive
no-preferencepattern already does. - On a hybrid device,
pointercan change mid-session when a mouse is paired. Rules chosen at load and cached in script will not update; the stylesheet will. - A user on a schedule-based dark mode gets a preference change at sunset, in a tab that has been open since morning (Long-Lived Clients and Version Skew).
- Supporting the preference matrix multiplies the states you have to design, review and screenshot. Light and dark, times high contrast, times forced colours, is four visual designs before anyone mentions a breakpoint.
- Answering in CSS means the outcome is not available to application logic without a second
matchMediacall, so a chart that needs its colours in JavaScript ends up with the preference expressed twice (Design Tokens). - Honouring reduced motion properly means designing two interaction stories — one with motion, one where the same state change is communicated another way — rather than deleting animations. That is real design work, not a media query.
- An override control plus a system default is more state than one setting: it has to persist, stay consistent across tabs, and not fight the OS when the OS changes (State Synchronization).
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe viewport, interaction and preference feature families are defined by the CSS Media Queries specifications and are broadly interoperable; what differs between engines is which features are implemented, not how a matching query behaves once it is.
- PLATFORM-SPECIFICWhether a preference is reported at all depends on the operating system exposing it: reduced motion and increased contrast come from OS accessibility settings,
forced-colorsis driven by a platform high-contrast mode that not every OS has, and an in-app webview may report none of them even when the host OS setting is on. - SPEC-EVOLVINGThis family is still growing — reduced transparency, reduced data, scripting and update features arrived at different times and sit at different levels of implementation, so check the current specification and your own field data rather than a tutorial's list of features.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Testing & Reliability Engineering — the preference matrix is a combinatorial test surface, and deciding which combinations earn a screenshot and which are covered by reasoning is the same triage as any other matrix.