ResponsiveGENERALBROWSER-SPECIFICDEVICE-SPECIFIC

The Viewport and Device Pixels

CSS pixels, device pixels and the ratio between them; the layout viewport versus the visual viewport; the one viewport meta tag worth writing — and why suppressing zoom is an accessibility failure, not a layout fix.

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.

The question

When I write 16px, what does that mean on a phone with a 3x screen, on a laptop at 150% OS scaling, and on a page a user has zoomed into?

The user intent

A person wants to read something. Sometimes that means holding the phone closer; sometimes it means pinching to magnify a table; sometimes it means running the whole browser at 200% because that is what their eyes need.

The obvious build

A pixel is a pixel. Copy the viewport meta tag from the last project — the one with user-scalable=no, which stops the annoying zoom when a form field is focused — and size everything in px so it matches the design file.

Why it breaks

A CSS pixel is not a hardware pixel and has not been for well over a decade. On a 3x phone, one CSS pixel covers nine device pixels, which is why a 1px hairline can render as a crisp thin line on one device and a fuzzy grey band on another.

How it breaks in a real browser
  • A CSS pixel is not a hardware pixel and has not been for well over a decade. On a 3x phone, one CSS pixel covers nine device pixels, which is why a 1px hairline can render as a crisp thin line on one device and a fuzzy grey band on another.
  • user-scalable=no and maximum-scale=1 remove pinch zoom. For a low-vision user on some platforms that is their primary magnification tool, and taking it away to prevent a cosmetic jump is trading someone's ability to read for your layout's tidiness.
  • The input-zoom behaviour the tag was copied to suppress has a real fix — give form controls a font size at or above the platform's zoom threshold — and suppressing zoom fixes the symptom by disabling a feature (Input Types, Inputmode and Autocomplete).
  • Without any viewport meta tag, a mobile browser assumes a desktop-width layout viewport and scales the whole result down, so a perfectly good responsive stylesheet renders as a shrunken desktop page and every media query reports the wrong width.
  • devicePixelRatio is not a hardware property. Browser zoom multiplies it, so the same laptop reports different values at different zoom levels, and code that uses it as "is this a retina screen" is wrong for every zoomed user (Fluid Layout First).
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • The CSS pixel is a reference unit, not a hardware one. It is defined so that content of the same CSS size appears at a similar physical size across devices at typical viewing distances — closer for a phone, further for a monitor — which is precisely why it cannot equal a device pixel.
  • The device pixel is one addressable dot on the display. devicePixelRatio is the number of device pixels per CSS pixel along one axis, and it is the *product* of the display's density and the current browser zoom.
  • The layout viewport is the box your layout is laid out into and what width media queries measure. The visual viewport is the part of it the user can currently see. Pinch zoom shrinks the visual viewport within an unchanged layout viewport; browser zoom shrinks the layout viewport itself.
  • <meta name="viewport" content="width=device-width, initial-scale=1"> says: make the layout viewport the width of the device in CSS pixels, and start at a scale of 1. Without it, mobile browsers default to a wide layout viewport and scale down.
  • user-scalable=no and maximum-scale=1 cap or disable the visual-viewport zoom. Some browsers now ignore them precisely because they were so widely misused, which means the tag is both harmful and unreliable (The Accessibility Tree).
  • The visualViewport API exposes the visual viewport's offset, size and scale, and fires events when it changes — which is how a page correctly positions a floating element when an on-screen keyboard opens or a user pinches.

What this makes the browser do

And which of it is avoidable.

  • Rasterising at the device pixel ratio: the browser paints into a bitmap scaled by the ratio, so a 3x screen paints roughly nine times the pixels for the same CSS-pixel area (Paint Commands).
  • That scaling is why paint cost is a device property as much as a page property. The same page is meaningfully more expensive to paint on a dense screen (What Is Actually Inside a GPU).
  • Browser zoom changes the layout viewport, so it triggers a full style and layout pass and re-evaluates every media query. Pinch zoom does not — it is a compositor transform over an already-painted layer, which is why it stays smooth (Compositing Layers).
  • A change in device pixel ratio — dragging a window between monitors — re-rasterises layers and can re-select srcset candidates (Responsive Images).
  • Sub-pixel positions are resolved at raster time. A box whose computed position falls between device pixels is anti-aliased, which is the mechanism behind blurry text in a transformed layer.

Three grids, all called pixels

Almost every confusion in this area comes from collapsing three separate coordinate systems into one word. Your layout is expressed in CSS pixels. The display addresses device pixels. And what the user can currently see is a window onto the layout that pinch zoom can move and resize independently of both.

The two arrows worth memorising are the ones from zoom. Browser zoom points at the layout viewport — it changes how many CSS pixels wide the page is, so media queries re-evaluate and the layout reflows. Pinch zoom points at the visual viewport only — the layout is untouched and the compositor magnifies what was already painted.

CSS pixels, viewports and device pixels
width=device-widthlayout resolves herechanges CSS-pixel width -> reflowmagnifies onlythe visible window onto itmultiplies itraster scaleCSS pixel — the unit your layout is written in<meta name="viewport">Browser zoomPinch zoomLayout viewport — what media queries measuredevicePixelRatio = density x zoomDevice pixels — actual addressable dotsVisual viewport — what the user can see now
UserLLMAgentToolDataDecisionHumanGuardrail

The viewport meta tag, minus the cargo cult

There is one tag worth writing and two variants worth understanding. Everything else in the version that circulates on the internet is either a default being restated or an accessibility regression being propagated.

The table underneath is the arithmetic that srcset performs for you, and it is worth reading once because it kills two beliefs at the same time: that pixel ratio is a hardware property, and that a phone always wants fewer pixels than a laptop.

What to write, and what not to
1<!-- The whole tag. This is all most pages need. -->
2<meta name="viewport" content="width=device-width, initial-scale=1">
3
4<!-- Blocks or caps pinch zoom. Do not ship this. Some browsers ignore it,
5 which means it is both harmful and unreliable. -->
6<meta name="viewport"
7 content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
8
9<!-- Only when the page genuinely paints under a notch or home indicator,
10 and only together with the safe-area insets. -->
11<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
12<style>
13 .app { padding-inline: max(1rem, env(safe-area-inset-left)); }
14</style>

With no viewport tag at all, a mobile browser assumes a wide desktop layout viewport and scales the rendered result down, so the page looks like a shrunken desktop site and every width media query reports a width the device does not have. width=device-width is the line that makes your media queries mean what you think they mean.

Layout                     CSS px   DPR   Device px   srcset picks
----------------------------------------------------------------------
Phone, 3x display, full-bleed   390     3        1170     ~1200w
Phone, 2x display, full-bleed   390     2         780      ~800w
Laptop, 1x, 1440 layout        1440     1        1440     ~1440w
Laptop, 2x, 1440 layout        1440     2        2880     ~2880w
Same 2x laptop at 200% zoom     720     4        2880     ~2880w

The browser never asks "what device is this". It computes:
  layout width in CSS px  (from `sizes`)  x  devicePixelRatio
  -> a target width in device px, matched against the `w` descriptors.

Read the last two rows together. Zoom added no pixels to the screen; it
relabelled them. Half as many CSS pixels across, twice as many device
pixels per CSS pixel, the same 2880 dots — which is why devicePixelRatio
is a rendering ratio and a terrible "is this a retina device" test.

Zoom is not a preference you get to override

The two zoom mechanisms do different things and users reach for them for different reasons. Browser zoom is "make everything bigger and rearrange"; pinch zoom is "magnify this part without changing anything". A page has to survive both, and only one of them is something a page can even influence.

The spec below is unusual for this domain in that it has no ARIA in it at all. The contract is with the browser: the layout viewport is authoritative, the page reflows into whatever it is given, and every control stays reachable.

accessibility specA page that survives zoom and magnificationWhat zoom requires of a page

semantics No roles and no ARIA. The requirement is structural: the layout must be expressible at any CSS-pixel width the browser hands it, and every control must remain reachable and operable at every scale (Fluid Layout First).

Ctrl/Cmd and + / -Browser zoom. Shrinks the layout viewport in CSS pixels, so media queries re-evaluate and the layout reflows. This is the mechanism WCAG reflow is about.
Pinch gestureVisual zoom. Magnifies what is already painted without changing layout. This is the one user-scalable=no blocks, and it is the primary magnifier for many low-vision users.
Ctrl/Cmd and 0Reset to 100% — worth knowing when your own testing starts producing results you cannot explain.
TabMust still reach every control at every zoom level. A control pushed outside a clipped container is unreachable, not merely unseen (Keyboard Operability).
Focus
  • A focused element must be scrolled into the *visual* viewport, not just the layout one. overflow: hidden on an ancestor and scroll-margin are the two things that usually decide whether this works.
  • Sticky and fixed chrome sized in vh consumes a growing share of the visual viewport as zoom increases, and can end up covering the element that just received focus.
  • Focus must not move when a zoom-triggered media query changes the layout. If a component is rebuilt at a breakpoint, restore focus deliberately (Focus Management).
Announces
  • Nothing new is announced by zoom itself — but content that exists only in one layout leaves the accessibility tree when zoom crosses a breakpoint, which reads to a screen-reader user as content disappearing.
  • Screen-magnifier users pan a small window across the page, so a value far from its label is effectively unlabelled. Keep related information physically close (Accessible Component Patterns).

usually broken by Shipping user-scalable=no or maximum-scale=1 to stop a mobile browser zooming when a form field is focused. That behaviour has a real fix — give form controls a font size at or above the platform's zoom threshold — and disabling zoom to avoid it removes a low-vision user's primary magnification tool to prevent a cosmetic jump.

How to build it

Most important first.

  • Write exactly one viewport meta tag: width=device-width, initial-scale=1. Add viewport-fit=cover only if the page genuinely paints under a notch or a home indicator, and then handle the safe-area insets.
  • Never ship user-scalable=no or maximum-scale=1. If a specific behaviour is bothering you, fix that behaviour — usually a form control font size, or a layout that assumes the visual viewport equals the layout viewport (Native Forms First).
  • Size in rem for anything that should scale with the user's reading preference, and in px only for things that are genuinely device-anchored, such as a hairline border (Responsive Typography).
  • Serve images by pixel budget rather than by device class: srcset already multiplies layout width by the ratio for you, which is more accurate than any devicePixelRatio branch you could write (Responsive Images).
  • Use dvh / svh / lvh rather than vh where a mobile browser's collapsing toolbar matters, and understand that they are three different answers to "how tall is the viewport" rather than three spellings of one (Grid: Two Dimensions at Once).
  • Listen to the visualViewport API — not window.resize — when positioning something against an on-screen keyboard or a pinched view (How an Event Is Dispatched).

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • Zoom is a right, not a preference to be overridden. WCAG requires that content can be resized substantially without loss of content or functionality, and separately that it reflows into a narrow viewport without scrolling in two dimensions. Suppressing zoom fails both.
  • Browser zoom is what triggers reflow: it shrinks the layout viewport in CSS pixels, so a fluid layout adapts and a fixed one produces a two-dimensional scroll (Fluid Layout First).
  • Pinch zoom is the magnifier: it does not reflow, it magnifies. A user panning a magnified view needs content whose meaning is local — a value far from its label is effectively unlabelled at high magnification (Accessible Component Patterns).
  • Text-only zoom and a raised default font size scale rem and em but not px. A layout sized entirely in pixels ignores the setting completely, which is why it looks fine to a developer who has never changed it (Responsive Typography).
  • A focused element must be brought into the *visual* viewport, not just the layout one. overflow: hidden on an ancestor, or a sticky header sized in vh, can leave the focused control permanently off screen at high zoom (Focus Management).

What can go wrong

Failure modes
  • A layout that assumes the visual viewport equals the layout viewport. A position: fixed bar sits at the bottom of the *layout* viewport, which after a pinch may be entirely off screen.
  • 100vh on mobile, where the browser toolbar collapses on scroll. The classic result is a "full height" section that is taller than the screen and a footer that can never quite be reached.
  • Canvas drawn at CSS-pixel dimensions on a high-density screen, producing a visibly soft image. The fix is to size the backing store by the ratio and scale the drawing context — and to redo it when the ratio changes.
  • Detecting "retina" from devicePixelRatio and shipping a branch, which then misfires for every user at non-default browser zoom.
  • Removing focus-visible outlines because they "look wrong at 2x", which trades a rendering nit for the ability to see where focus is (Keyboard Operability).
  • The mitigation failing: adding viewport-fit=cover without using env(safe-area-inset-*), so content is now drawn underneath the notch and the home indicator instead of merely beside them.
What can arrive out of order
  • A pixel-ratio change from moving a window can arrive after images have been chosen, so the page may briefly display a candidate selected for the old ratio (Responsive Images).
  • An on-screen keyboard opening and a scroll gesture can arrive in either order, and a floating element positioned from stale visual-viewport values lands in the wrong place for a frame.
  • Browser zoom triggers a relayout that can complete after a script has already read a width, so any cached viewport measurement is stale from the moment zoom starts (Layout Thrashing).
Security
  • Viewport size, pixel ratio and zoom level are all readable by any script in the page and are strong fingerprinting inputs, particularly in combination — a non-default zoom is unusual enough to be identifying (Session Replay and the Privacy It Costs).
  • The visual viewport is what the user can see, and a control positioned outside it is invisible but still clickable — the same shape as a clickjacking overlay, arrived at by a layout that assumed the two viewports were one (Clickjacking and Framing).
  • The browser will not prevent a page from making itself unusable at high zoom. Nothing here is enforced; it is entirely on the page to remain operable, and only testing will tell you whether it does.
Misreads
  • "devicePixelRatio tells me the screen density." It tells you device pixels per CSS pixel *right now*, which includes browser zoom. It is a rendering ratio, not a hardware fact.
  • "user-scalable=no is fine because we made the site responsive." Responsiveness handles reflow; it does not replace magnification. A user who needs 500% on one table still needs to pinch.
  • "The layout viewport and the visual viewport are the same thing." They are the same only at scale 1 with no pinch, which is the state every developer works in and a large minority of users do not.
  • "vh means the height of the screen." It means a viewport height whose definition on mobile depends on whether the toolbar is counted, which is exactly why svh, lvh and dvh exist.
  • "CSS pixels are a legacy unit; real work uses rem." rem is defined as a multiple of the root font size, which is itself in CSS pixels. The CSS pixel is underneath everything.

Measuring it, and what changes in the field

How you would see this
  • Zoom your own browser to 200% and then 400% and try to complete a task. This is the measurement; there is no tool that replaces it (A Method for Frontend Bugs).
  • The device toolbar can emulate a device pixel ratio independently of viewport size, which is the only convenient way to check raster quality and srcset selection from a 1x laptop.
  • Log window.devicePixelRatio, window.innerWidth and visualViewport.scale while zooming to see which of the three actually moves — it is the fastest way to internalise the layout/visual distinction (A Mental Model of the Devtools).
  • A headless check that fails when a rendered page scrolls horizontally at a narrow width catches most reflow failures cheaply (Visual Regression Testing).
  • In the field, record the distribution of pixel ratios and viewport widths. It is usually wider than anyone expects, and it retires several arguments at once (Real User Monitoring).
Slow device, slow network, large data, old tab
  • On a dense screen, paint and raster cost scale with the ratio squared for the same CSS area, so a low-end phone with a high-density display is the worst combination in the field (The Frame Budget).
  • On a device with an on-screen keyboard, opening it changes the visual viewport without necessarily changing the layout viewport, and pages that listen only to window.resize will not notice (Form State Is a Draft).
  • A window dragged between a built-in display and an external monitor changes the pixel ratio mid-session, re-rasterising layers and possibly re-fetching images (Long-Lived Clients and Version Skew).
  • At high OS-level display scaling, the browser reports a ratio that is neither 1 nor 2 — fractional ratios are ordinary, and code branching on integers breaks on them.
  • On a slow network the difference between candidates chosen by ratio is the difference between a fast page and a slow one, which is why the ratio is an input to image selection rather than a styling concern (Bandwidth vs Latency).
What this costs
  • Respecting zoom means designing for viewport widths that no device has — a desktop layout at 400% is roughly a phone width with desktop content in it, and that combination has to actually work.
  • Handling the visual viewport properly means an event listener and some positioning logic that only ever matters to a minority of sessions, and which is easy to regress because nobody on the team routinely pinches.
  • dvh and friends solve the collapsing-toolbar problem by making the unit change as the toolbar moves, which means a layout using them can itself shift during scroll. There is no unit that is both stable and accurate here (Visual Stability).
  • Sizing everything in rem means the whole layout responds to a user's font-size setting, which is correct and also means a large setting can produce layouts nobody has ever seen. Test at least one.

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 CSS pixel as a reference unit, the layout/visual viewport split, and the meaning of devicePixelRatio are specified behaviour and consistent across engines; the disagreements are about defaults and about how the mobile toolbar affects viewport units.
  • BROWSER-SPECIFICHandling of user-scalable=no and maximum-scale diverges deliberately: some browsers honour them, others ignore them to protect users from exactly this misuse, so the tag produces different behaviour on different browsers and cannot be relied on even by someone who wants it.
  • DEVICE-SPECIFICMobile browsers collapse and expand a toolbar during scroll, which is why svh, lvh and dvh differ there and are all identical on a desktop browser where no toolbar overlays the viewport — so a vh bug is invisible on the machine most people build on.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

Domains that do not exist yet
  • Programming Languages & Runtime Internals — the fractional arithmetic behind sub-pixel layout and raster snapping is ordinary floating-point behaviour, and the "why is this one pixel off" bug is usually a rounding boundary rather than a CSS one.
OS & Networkingbandwidth-vs-latency