Skip to main content.

Targeting smartphones with CSS

As of this writing, this site uses the following CSS media queries to target smartphones in portrait orientation:

@media only screen and (hover: none) and (max-aspect-ratio:3/5) {

The max-aspect-ratio query ensures that we only target devices in portrait orientation. We use only screen and (hover: none) to detect devices that have screens but not mice.

So why this particular set of queries? Well, there's no exact way to detect smartphones using CSS media queries. Actually, let's be more specific: there's no exact way to detect high-resolution, small-size, touch-only devices using CSS media queries or similar features.

Approaches that don't work

In media queries, the handheld device type is deprecated, and current standards require that it match no device.

To quote the Media Queries Level 4 spec, the resolution media query "does not refer to the number of device pixels per physical length unit, but the number of device pixels per CSS unit." The special dpi unit usable in these queries, then, doesn't actually describe the dots per inch — the number of physical pixels per physical inch — but rather the number of device pixels per CSS inch, which isn't the same thing for reasons we'll get to shortly.

Targeting touch devices won't meet our use case either. Not all touch-capable devices are smartphones; tablets and even many laptops have touch-capable screens. The nearest analogue is to test for devices that are hover-incapable despite having screens.

So why is there no better option?

Browsers lie about DPI

The CSS standard definition of a "pixel" is somewhat counterintuitive — a pile-up of historical compromises and assumptions with a result that isn't entirely optimal today. The standard mandates fixed relationships between all length units, physical and digital, and assumes a DPI of 96 in order to allow this; ergo 96px is equivalent to 1in regardless of the actual properties of the device displaying the page. Devices which support CSS are allowed to implement these fixed relationships in one of two ways, choosing at their discretion:

  • Map 1px to a single device pixel, thereby making all physical units (e.g. 1in) inaccurate for DPIs other than 96. This implies a need for the device to lie to pages and JavaScript about the DPI, screen size, and similar, in order to maintain the 96 DPI conversion factor.
  • Map 1px to one ninety-sixth of a physical inch, thereby making the basic pixel unit inaccurate on devices with DPIs other than 96: 1in is accurate, but 1px no longer maps to a single hardware pixel.

The term for this is "anchoring:" you anchor either the physical units to the digital, or the digital to the physical. The standard encourages screen devices to make the former choice, and print media to make the latter choice. However, browsers can choose at their discretion.

Of course, on mobile, the unit mapping becomes even more skewed thanks to the meta viewport tag... and often, mobile user-agents will supply default values for meta viewport behavior even if the tag isn't present.

<meta viewport> is counterintuitive and unreliable

The meta viewport tag allows you to set several mobile-related parameters, including width and height parameters that are meant to control how CSS pixels map to device pixels. Simply put: you can control how large 100vw and 100vh are. However, this mapping is not straightforward.

Let's consider the implementation of meta viewport just on Google Chrome. When you use device-width and device-height, Chrome takes the device pixel resolution and converts it to 160 DPI, to get a result measured in dip, an Android-specific unit called "density-independent pixels." They then shear off the unit, take these numbers as CSS pixel quantities, and use that as the viewport size.

Let's look at an example from Chrome's developer documentation.

  • We start with the device pixel resolution. This may optionally exclude regions of the screen reserved for use by the OS-level UI, such as a tablet or phone's status bar and bottom action bar. For the example here, we're using the 2012-era Nexus 7; it has a device pixel resolution of 800x1280px, but Google here lists it as 800x1205px.
  • We next need the screen's "density." Google does not here specify what that means, but looking at the Android API, they're referring to the DisplayMetrics.density value. This value is used as the scaling factor for the dip unit: divide the device pixel resolution by this value to get the resolution in dips.

    Per the documentation, dips assume a resolution of 160 DPI; on such a screen, the density is 1.0. Google states that the Nexus 7 has a density of 1.33; if you multiply 160 by 1.33, the result is 212.8. This result is pretty much accurate: it's the approximate DPI of the Nexus 7 given a device pixel resolution of 800x1205 and a diagonal of 6.8 inches (the real diagonal is 7 inches, but remember: Google is excluding parts of the screen from these measurements).

  • Dividing the device pixel resolution by the density value, we get the screen's size in density-independent "pixels:" 600x900dips. Shear off the unit, and these are used as the device-width and device-height.

This means that contrary to what one would expect given their names, device-width and device-height are not the device pixel resolution — the true physical resolution of the device — but rather are the same kind of abstraction as CSS pixels, but with a different chosen DPI. The abstracted size is vaguely connected to the device's real resolution and real DPI, but the difference in the assumed DPIs (between meta viewport and CSS itself) adds another hurdle to jump over.

There's one other problem that a web designer might run into: if the page contains an element that overflows the specified viewport width, then browsers will expand the viewport width to ensure that that element fits. In these scenarios, however, 100vw still refers to the non-expanded width i.e. the intended viewport width rather than the actual viewport width; 100vw becomes narrower than the viewport.

In summary

  • On platforms where meta viewport is used (including platforms which use meta viewport behavior with some default width, e.g. 960 or 980, when the tag is omitted), the width option controls how many CSS pixels map to 100vw. By implication, 1px on these platforms never maps directly to one device pixel.
  • On Google Chrome, at the very least, setting width=device-width means that 160px is equivalent to one physical inch, and 100vw is equivalent to the full width of the screen, assuming the page doesn't overflow. That choice of DPI violates the CSS spec, but the behavior overall at least keeps to the spirit of the spec, anchoring digital units to physical ones as would be done for print media.
  • Setting width to any other value means that there's no longer any mapping between any CSS unit and any real-world unit: nothing is anchored to anything; the measurement system is completely unmoored from reality. This is a violation of the CSS spec. It's also the default behavior in pretty much every mobile browser, since again, most of them default the viewport width to 960 or 980.

That middle bullet point almost sounds like something you could use in order to size things based on the device's real-world pixel density, though you'd only be able to size things in inches and not in device pixels. The problem is that there's no way to detect, from CSS alone, whether meta viewport is actually in use, let alone whether the viewport width is device-width. There are JavaScript hacks to do it, but redoing all of your element sizes, throughout your stylesheet, in inches based on JavaScript strikes me as a bad idea.

As a side note: Mozilla Developer Network's current documentation for meta viewport is wrong about device-width and device-height as of this writing. They describe device-width and device-height as being equivalent to 100vw and 100vh, which is incorrect. The purpose of the width and height parameters is to control what 100vw and 100vh are, so any value you use is 100vw and 100vh, and any value you don't use isn't. If you use a number that isn't what device-width would've computed to, then device-width isn't 100vw.