Calibration with a physical ruler
The browser doesn't know the real size of the screen. Use a real ruler on the glass and match the virtual ruler to it. Once they line up, we know the DPI exactly.
No ruler? You can use a bank card, which has a fixed size of 85.60 × 53.98 mm (the ISO ID-1 standard). On the horizontal ruler, bring its long edge (85.60 mm) onto the orange line. On the vertical one, rest the card on the orange box until they match.
Horizontal DPI: —
Vertical DPI: —
Effective DPI
Three densities, three different questions
Horizontal DPI
Vertical DPI
DPR
Pinch zoom (visualViewport)
Physical size
Diagonal
| pixel type | 1 pixel in | 1 mm in | ||
|---|---|---|---|---|
| inch | mm | pt | pixel | |
| css pixel | 1 / — | — | — | — |
| logical pixel | = css1 | — | — | — |
| device pixel | 1 / — | — | — | — |
| hardware pixel | = device2 | ? | ? | ? |
1 at 100% zoom · 2 when the system renders at the native resolution
Do you know your screen's real (native) resolution?
Optional. The browser has no way to learn it, and at the current DPR a genuinely high-density panel is indistinguishable from an image drawn large and then scaled down. Declare it and that is settled. You will find it in your system's display settings.
Common values:
The hardware pixel is taken to equal the device pixel, i.e. we assume the image reaches the screen with no intermediate scaling.
Native panel resolution: — declared
Your declaration stays locally in your browser (localStorage) and is not sent anywhere.
The browser doesn't know the physical size of the screen. With the ruler you give it that information: when the virtual marks fall on the real ones, we know how many CSS pixels cover 1 real centimetre. That figure, call it p (px/cm), is the whole basis of the calculation.
Four 'pixels' that often get confused, from the most abstract to the most physical:
| Name | What it is | Fixed? | Do we have it? |
|---|---|---|---|
| css pixel | the browser's logical unit for layout | No, changes with zoom | ✓1 |
| logical pixel | OS logical pixel (point / DIP) | No, changes with scaling | ✓ |
| device pixel | the surface the browser renders to (css/logical × DPR) | No, changes with scaling + zoom | ✓2 |
| hardware pixel | the hardware dot, the screen's native resolution | Yes, baked into the hardware | ✗3 |
1 css pixel: you measure it here, with the ruler · 2 device pixel: computed, not the panel itself · 3 hardware pixel: no access, we don't measure it
The browser does not see the physical screen as it really is. Its physical size (how many inches) is entirely unknown to it, while it knows the resolution in CSS pixels (window.screen), affected by the operating system's scaling and by zoom, not the device pixels. The DPR takes us to the device pixels, but it blends the screen's density together with the zoom.
So because the physical size is missing, the DPI cannot be derived on its own: we measure it with the ruler. The result is the effective DPI, the density of the device pixels (how many per inch) on the glass (equal to the hardware pixels when the system renders at the native resolution, with no intermediate scaling), which often differs from what the browser «assumes» (the CSS default of 96 ppi*).
* CSS fixes 1 inch = 96 CSS pixels (so 1px = 1/96 inch, 1pt = 1.333px, 1cm ≈ 37.8px). So for CSS purposes the browser «assumes» every screen is 96 ppi, regardless of its real density. If you write width: 1in, you get 96 CSS pixels, not necessarily a real inch on the glass.
In the «Three densities» area above, when the values diverge, it is tempting to read the smallest one as a ceiling, for example: «CSS ppi 100, so whatever the system does, 100 is what I get». It is not. CSS ppi is not a density of detail, it is a scale: the coordinate system CSS is written in. The dots on the glass stay as many as they are, and all of them are used.
Take, for example, a 125 ppi screen at 125% scaling with DPR 2, and a hairline on it, border: 1px. In the logical layout it is 1 CSS pixel. The browser paints it 2 pixels wide on the 200 ppi surface. The compositor scales that down, and the line ends up covering 1.25 real pixels of the panel: it is not rounded into one fat «box of 100», it lands on about one and a half physical pixels with one edge half-lit. A 16px letter gets 20 real dots of height, not 16.
So the two questions have different answers: how big something looks is set by the CSS ppi, how sharp it is by the panel's density. The effective DPI in the middle never reaches the eye: it is an intermediate surface, thrown away in the downscale. You gain smooth curves instead of stair-steps, and you pay a slight blur, because the rendered pixels do not land whole on the physical ones.
One exception, and it is a real one: the ready-made image. Text and vectors are painted onto the large surface from the start, so they use the panel's full density. A raster, though, carries its own detail and no scaling adds to it: served at 100 ppi it stays at 100 ppi, and the panel's remaining 25 go unused. There, and only there, the ceiling really is the CSS ppi. That is what srcset with a 2x variant is for.
One inch = 2.54 cm, and each CSS pixel maps to DPR device pixels. So the device pixels per inch (the DPI) are:
With pinch-zoom (touch or trackpad) the visualViewport.scale factor is added too; page zoom (Ctrl +/−) is already inside the DPR:
We measure it separately for the horizontal and the vertical ruler and show the average:
In your measurement: p ≈ — px/cm · DPR — → DPI ≈ — ppi
Knowing how many CSS pixels = 1 inch (that is p × 2.54, the «CSS ppi»), the physical width comes from dividing the screen resolution (in CSS pixels) by it. Likewise the height:
And the diagonal from the Pythagorean theorem:
In your measurement: w ≈ —″ · h ≈ —″ · D ≈ —″
Note: the physical size and the diagonal hold at the current zoom. The browser's page zoom (Ctrl +/−) shifts them, because window.screen.width does not follow the calibration when the zoom changes (and the zoom level is not detectable from the page). For an accurate value, measure at 100% zoom — the DPI stays correct anyway.
On high-density (retina) screens a CSS pixel is not one device pixel; it is DPR device ones (e.g. 2×2 = 4). The ruler measures in CSS pixels, so to reach the device pixels we multiply by the DPR. Without it, the DPI would come out half (the «CSS ppi»).
On a computer, page zoom (Ctrl and + or −) changes the DPR: at 200% the DPR doubles. But the browser does not expose a separate «zoom level», partly for privacy reasons (to prevent fingerprinting), so we only see the combined DPR, which mixes the screen density with the zoom. This is exactly why the ruler calibration is needed: only that way can we separate the real DPI from the zoom.
On tablets and phones, pinch-zoom does not change the DPR (it stays fixed, at the device's native value), but we read it from visualViewport.scale. The same holds for a trackpad pinch on a computer. So whichever way you zoom, the calculation stays correct: we multiply the DPR by visualViewport.scale (where available) to find how many device pixels one CSS pixel finally maps to.
Right now on the page: DPR — · visualViewport.scale —