Tags: web-dev concept

Viewport and Responsive Behaviour

Date: 2026-08-17


A CSS pixel is not a device pixel and hasn’t been for fifteen years. The viewport meta tag is what tells a mobile browser to stop pretending it’s a desktop, and the device pixel ratio is what decides how many physical pixels an image needs — get either wrong and the page is unreadable or three times heavier than it should be.


The viewport is the rectangle the browser lays the page out in, measured in CSS pixels; device pixel ratio (DPR) is how many physical screen pixels make up one CSS pixel.

The two viewports

Mobile browsers maintain a fiction, and the meta tag is how you decline it.

WITHOUT the viewport meta tag

  layout viewport = ~980 CSS px      ← the browser pretends it's a desktop
  visual viewport = ~390 CSS px      ← what the user can actually see
  → the page renders at desktop width, then zooms out to fit
  → text is unreadable, media queries never match

WITH <meta name="viewport" content="width=device-width, initial-scale=1">

  layout viewport = 390 CSS px       ← matches the device
  visual viewport = 390 CSS px
  → media queries work, text is legible
<meta name="viewport" content="width=device-width, initial-scale=1">

That exact line, and nothing more. Two additions to avoid:

<!-- ✗ both prevent pinch-zoom. an accessibility failure, and a
     WCAG conformance failure for anyone who needs to magnify -->
<meta name="viewport" content="width=device-width, maximum-scale=1">
<meta name="viewport" content="width=device-width, user-scalable=no">

Preventing zoom is usually added to stop iOS zooming when a form field is focused. The actual fix for that is a font size of 16px or larger on inputs — iOS zooms only below that threshold — which solves it without removing a control people depend on — WCAG, Accessible Forms.

Device pixel ratio

DPR = physical pixels / CSS pixels

DPR 1    1 CSS px = 1 device px      older desktop monitors
DPR 2    1 CSS px = 4 device px      most modern phones, retina laptops
DPR 3    1 CSS px = 9 device px      high-end phones

a 400 × 300 CSS px image slot on a DPR 2 screen
  → needs an 800 × 600 source to look sharp
  → that's 4× the pixels, not 2×

The quadratic relationship is the thing to hold. Going from DPR 1 to DPR 3 is nine times the pixel data, which is why serving one large image to everyone is so expensive on the devices least able to afford it.

The fix is srcset with density or width descriptors, letting the browser choose:

<img src="/product-400.jpg"
     srcset="/product-400.jpg 400w,
             /product-800.jpg 800w,
             /product-1200.jpg 1200w"
     sizes="(max-width: 600px) 100vw, 400px"
     width="400" height="300" alt="…">

sizes describes the slot, not the image. It tells the browser how wide the element will be at each breakpoint, so it can multiply by DPR and pick a source — before CSS has loaded, which is why it can’t work this out for itself — Image Optimisation, Modern Image Formats.

The viewport units, and the mobile problem

vw / vh      1% of the LAYOUT viewport
             vh is the trap: on mobile it's the viewport WITHOUT
             the address bar, so a 100vh element is taller than
             the visible area until the bar hides

svh          small viewport height  — address bar VISIBLE
lvh          large viewport height  — address bar HIDDEN
dvh          dynamic — changes as the bar shows and hides
             ← use for full-height layouts. note it triggers
               layout on scroll, so don't animate against it

height: 100vh on a mobile hero is the classic bug — content sits below the fold because the browser reserved space for a bar that’s currently drawn over it. 100svh is usually what was meant.

Breakpoints and the better alternatives

Breakpoints respond to the viewport, which is the wrong question for a component that could appear in a sidebar or a full-width row.

MEDIA QUERY                       CONTAINER QUERY

@media (min-width: 768px)         @container (min-width: 400px)

responds to the viewport          responds to the component's own
                                    container
a card in a narrow sidebar        the card knows how much room IT has
  gets the "wide" layout          → correct in every context
  because the window is wide

Container queries remove most breakpoint logic in a component-based system, and they’re the right default for anything reusable. Viewport media queries remain correct for page-level layout — how many columns the whole page has.

Intrinsic sizing removes more of it again. min(), max(), clamp(), grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) and flex-wrap all adapt continuously without a single breakpoint — which is why the modern advice is breakpoints as a last resort rather than as the structure — Responsive Design.

Practical points

  • Test at 320px wide. It’s the narrowest viewport in common use and where horizontal overflow appears
  • Test at 400% zoom, which WCAG requires to reflow without horizontal scrolling — and which behaves like a very narrow viewport, so fixing one usually fixes the other
  • overflow-x: hidden on the body is a plaster. It hides the symptom; something is still wider than the viewport, and on some devices it breaks scroll behaviour. Find the offending element
  • Set width and height on every image so space is reserved before the file arrives — Cumulative Layout Shift
  • Respect safe areas on notched devices with env(safe-area-inset-*), or fixed bottom bars sit under the home indicator
  • The visual viewport moves independently when the on-screen keyboard opens. visualViewport is the API for anything that must track it

Where it interacts