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.
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?
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.
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.
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.
- 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=noandmaximum-scale=1remove 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.
devicePixelRatiois 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).
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.
devicePixelRatiois 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=noandmaximum-scale=1cap 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
visualViewportAPI 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
srcsetcandidates (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.
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.
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.
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 gesture | Visual 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 0 | Reset to 100% — worth knowing when your own testing starts producing results you cannot explain. |
| Tab | Must still reach every control at every zoom level. A control pushed outside a clipped container is unreachable, not merely unseen (Keyboard Operability). |
- — A focused element must be scrolled into the *visual* viewport, not just the layout one.
overflow: hiddenon an ancestor andscroll-marginare the two things that usually decide whether this works. - — Sticky and fixed chrome sized in
vhconsumes 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).
- — 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. Addviewport-fit=coveronly if the page genuinely paints under a notch or a home indicator, and then handle the safe-area insets. - Never ship
user-scalable=noormaximum-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
remfor anything that should scale with the user's reading preference, and inpxonly for things that are genuinely device-anchored, such as a hairline border (Responsive Typography). - Serve images by pixel budget rather than by device class:
srcsetalready multiplies layout width by the ratio for you, which is more accurate than anydevicePixelRatiobranch you could write (Responsive Images). - Use
dvh/svh/lvhrather thanvhwhere 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
visualViewportAPI — notwindow.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
remandembut notpx. 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: hiddenon an ancestor, or a sticky header sized invh, can leave the focused control permanently off screen at high zoom (Focus Management).
What can go wrong
- A layout that assumes the visual viewport equals the layout viewport. A
position: fixedbar sits at the bottom of the *layout* viewport, which after a pinch may be entirely off screen. 100vhon 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
devicePixelRatioand 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=coverwithout usingenv(safe-area-inset-*), so content is now drawn underneath the notch and the home indicator instead of merely beside them.
- 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).
- 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.
- "
devicePixelRatiotells 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=nois 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.
- "
vhmeans the height of the screen." It means a viewport height whose definition on mobile depends on whether the toolbar is counted, which is exactly whysvh,lvhanddvhexist. - "CSS pixels are a legacy unit; real work uses
rem."remis 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
- 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
srcsetselection from a 1x laptop. - Log
window.devicePixelRatio,window.innerWidthandvisualViewport.scalewhile 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).
- 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.resizewill 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).
- 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.
dvhand 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
remmeans 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
devicePixelRatioare 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=noandmaximum-scalediverges 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,lvhanddvhdiffer there and are all identical on a desktop browser where no toolbar overlays the viewport — so avhbug 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.
- — 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.