CSS font size - it's not what it seems

Ever since I started using a 4K monitor, I noticed that I have to adjust browser zoom almost for every site where I wanted to read content and not just glance at some excerpt. Looking closer at CSS styles on some of those sites I found that most of them used pixel unit sizes for pretty much everything, including fonts. I was puzzled at first by the widespread use of a technique that cannot compute sizes in a predictable way across various displays and devices, but further investigation confirmed that it was done on the advice of the CSS specification.

W3C Guidance

CSS Values and Units Module Level 4

For screen media (including high-resolution devices), low-resolution devices, and devices with unusual viewing distances, it is recommended instead that the anchor unit be the pixel unit. For such devices it is recommended that the pixel unit refer to the whole number of device pixels that best approximates the reference pixel.

Reading through the CSS specification some more, it got even more complicated from the previous definition, which used to define the reference pixel based on the 96 dpi resolution, and now the reference pixel is no longer tied to any physical pixel measurement via a fixed ratio, but rather is defined as an angle.

The reference pixel is the visual angle of one pixel on a device with a pixel density of 96dpi and a distance from the reader of an arm’s length. For a nominal arm’s length of 28 inches, the visual angle is therefore about 0.0213 degrees. For reading at arm’s length, 1px thus corresponds to about 0.26 mm (1/96 inch).

This seems reasonable when one looks at different device types, such as a wall-mounted TV, a computer display or a phone display, which all have different reference pixel sizes because the viewing distance for each device is different. However, this definition does not answer how to measure pixels on devices with various resolutions and physical sizes that are viewed at the same distance, such as a 4K display attached to a laptop with a smaller screen.

In fact, this definition looks a lot like Microsoft's effective pixels for universal Windows apps, except that Microsoft can figure out the viewing distance from the type of device running the app, while as far as CSS is concerned, it's just the user agent that may be running on a TV or a phone or a computer.

I looked through various parts of the CSS specification and could not find any definitive answer on how to specify size measurements in such a way that it would produce predictable physical sizes, similar to how font sizes in points work in printing, so I decided to do a couple of experiments to see how it works.

Test Configuration

For these tests I used a laptop running Windows 10 with an external 4K display. Windows was configured with 150% display scaling for the laptop display and with 200% display scaling for the external 4K display. No browser zoom.

The external display measures 3840x2160 pixels, with pixel pitch 0.1554 mm, from the display specification. The laptop display measures 1920x1080 and only the diagonal measurement of 33.8 cm is provided in the specification.

From the 16:9 display ratio the diagonal length is 18.36 in aspect ratio units. Diagonal size 33.8 cm / 18.36 = 18.412 mm per aspect ratio unit, which for a 16:9 display translates into 16 * 18.412 = 294.592 mm and 9 * 18.412 = 165.708 mm for display width and height. Finally, 294.592 mm / 1920 pixels and 165.708 mm / 1080 pixels yields 0.1534 mm pixel pitch for the laptop display.

The external display pixel pitch yields pixel density as 25.4 mm / 0.1554 = 163.449 dpi and the laptop pixel pitch yields pixel density as 25.4 mm / 0.1534 = 165.580 dpi. It is also worth noting that the external display's pixel density ratio to the base 96 dpi is 1.703 and the same laptop's ratio is 1.725.

Lines, Lines, Lines

I started with a very simple test of displaying a one-inch div with a 1px border and see how it displays on both screens in Firefox, as shown below.

height: 1in; border-left-width: 1px

My quiet hope that between the browser and Windows they would figure out pixel density and I would see a one-inch line didn't work out and the line was shorter than one inch on the laptop screen and longer than one inch on the external display, even though both had similar physical pixel density. It was clear at this point that the actual length of the line was affected by each display's scaling setting.

I took a picture of my display with my camera, close enough that I could see physical dots on the display and then I took a screenshot with Alt-Print-Screen and, much to my delight, noticed that Windows captures physical device pixels in those screenshots, not scaled up display pixels, which made it simpler to troubleshoot line sizes.

Armed with this knowledge, I took a screenshot of the browser window on each display and things got a bit clearer - the line on the image captured on the external display was 2 pixels wide and 192 pixels long and on the laptop it was 1.5 pixel wide (using scaling) and 144 pixels long, which matched the 200% and 150% scaling for each display for a 96 dpi screen.

This means that Firefox uses Windows display scaling as the ratio against the base 96 dpi display, so a 96px line below looks exactly like the one-inch line above on both displays.

height: 96px; border-left-width: 1px

Checking the value of window.devicePixelRatio on the external display produced 2.0 and on the laptop screen it was 1.5, which matches each display's scaling setting.

It is always nicer to be able to configure font and other element sizes via CSS, rather than reconfigure UI based on window.devicePixelRatio, so I tested CSS media queries with resolutions that matched display scaling values.

/* 96 dpi @ 150% */
@media (resolution: 144dpi) {
p#css-res:before {content: "144 dpi: "}
}
/* 96 dpi @ 200% */
@media (resolution: 192dpi) {
p#css-res:before {content: "192 dpi: "}
}

This worked as expected in Firefox and the css-res paragraph was prefixed with 144 dpi on the laptop screen and with 192 dpi on the external display.

These tests indicated that Firefox uses 96 dpi as the base resolution and multiplies it by the display scaling factor to derive the final resolution, which has no mathematical relation to the actual display resolution and is only usable because people end up scaling their displays to be able to read comfortably.

In my case, my external display's physical resolution is 1.703 times greater than 96 dpi, so when I set display scaling to 175%, those 96px lines above come out close to one inch.

Images

One of the reasons for the advice to use pixel units in CSS is that it aligns well with images, which are measured in pixels. The div below is 96px in height and it contains a 192x96 pixel image with a blue background. Both, the div and the image have one-pixel side borders.

192x96px image > < 192x96px image

The image aligns well with equivalently-sized div, which means that even though the image is sized in pixels, it is not actually displayed as sized, but rather in computed pixel units. This may seem subtle, but for some people this will show their images worse than they intended. Specifically, a photograph scaled up two times may look like a nightmare for a photographer who posted it.

The only way to display such image as intended, image-pixel-for-physical-pixel is to use window.devicePixelRatio to resize the image dynamically to its actual size, which will make it smaller than surrounding elements, but will not introduce image scaling artifacts.

Chrome

When I tested the same HTML/CSS constructs in Chrome, the 96px-long div and image lines were rendered as 231px-long lines on the external display and as 172px-long lines on the laptop screen. Checking the value of window.devicePixelRatio indicated that Chrome was using a 2.4 ratio for the 200% display scaling and a 1.8 value for the 150% display scaling.

It puzzled me at first because the value wasn't matching any common ratios, but then I realized that my Windows text size is bumped up by 20% in Settings > Ease of Use > Make text bigger, which mostly affects Windows-rendered components, such as application menus, and Windows applications, such as File Explorer, but it turns out that Chrome adds this value on top of the display scaling value, which resulted in different results, compared to Firefox.

It is also worth noting here that the W3C reference pixel defined as 0.26 mm for viewing content at arm's length didn't seem to be observed by either Chrome or Firefox, as the ratio of reference pixel to the physical pixel on my external display, which is 0.26 / 0.1554 = 1.67, wasn't close to any of the ratios I observed in these tests.

This extra ease-of-use 20% adds one complication to the mix in that CSS resolution queries with Chrome may not match any specific resolution value, probably because Chrome is not rounding the resulting value up or down. So, for the laptop display, I had to use this CSS query to make it match. It appears that Chrome computes it as 96px * 1.8 = 172.8 dpi and it didn't match on 172, 173 or 174 dpi.

@media (min-resolution: 172dpi) and (max-resolution: 173dpi) {
p#css-res:before {content: "172-173 dpi: "}
}

Having said that, even though I used these specific queries for testing, a more practical approach would be to use broader resolution bands anyway.

Final Thoughts

The print industry figured this out long time ago and they can predictably print text of any size. Print people do get confused around images and keep asking photographers for 300 dpi images, which do not exist, as images simply get tagged with the intended print resolution, but otherwise are measured in pixels. Other than that, it works.

Text in some of my books ranges between 10 and 14 points, which translates into line height between about 3.5 and 4.9 mm and is comfortable to read. W3C recommendation to use pixel units for screen media results in many UI developers using pixel units of their displays and not reference pixel units. This used to work when most displays had resolution of 96 dpi, but now things get much more complicated with display scaling, OS accessibility scaling and a range of physical screen resolutions and sizes.

Moreover, I am not even sure the base W3C premise of a reference pixel defined as different size for different viewing distances is practical, because it is based on how some web application will be used and not how it is written. In other words, somebody may run a slide show in a web app on their TV and then use the same web app on their laptop to discuss their photos with other people. Same app, different viewing distance, different app components, making it impossible to use the same reference pixel size in both components.

I don't think W3C did a good job on this. They should have standardized rendering in physical measurements, such as centimeters, inches, etc, and let browser developers, OS developers and display manufacturers figure out how to render one millimeter in the code as exactly one millimeter on the screen.

For images, a more elaborate sizing would be needed because images are not created equal. Images embedded in text may be scaled along with that text, but photographs need to remain at the image resolution defined by the artist when they published that photograph, for example.

I realize that there is a lot of history in this, but since W3C decided to introduce a breaking change into how they define the reference pixel anyway, I wish that breaking change would bring us closer to the kind of screen rendering the print industry enjoyed for generations. Instead, the reference pixel got redefined in a way that will confuse more people and will bring even more browser zoom into web surfing.

Comments: