Targeting smartphones with CSS
As of this writing, this site uses the following CSS media queries to target smartphones in portrait orientation:
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, but1px
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 thedip
unit: divide the device pixel resolution by this value to get the resolution indip
s.Per the documentation,
dip
s assume a resolution of 160 DPI; on such a screen, thedensity
is1.0
. Google states that the Nexus 7 has a density of1.33
; if you multiply 160 by 1.33, the result is212.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
anddevice-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 usemeta viewport
behavior with some default width, e.g. 960 or 980, when the tag is omitted), thewidth
option controls how many CSS pixels map to100vw
. 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 that160px
is equivalent to one physical inch, and100vw
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
.