The cascade is what makes CSS special and powerful. But in the case of media queries, overlap can seem problematic.
Consider the following CSS (continuing rules for CSS media query overlap):
/* Standard - for all screens below 20em */
body { color: black; font-size: 1em; }
/* Query A - slightly wider, mobile viewport */
@media (min-width: 20em) and (max-width: 45em) {
body { color: red; } /* supposed to be unique for this width */
}
/* Query B - everything else */
@media (min-width: 45em) {
body { font-size: larger; } /* because viewport is bigger */
}
So when the screen is exactly 45em wide, the overlap at 45em will be treated according to the standard CSS cascade:
- All
max-width: 45em
definitions will be applied first,
- and all
min-width: 45em
will be applied thereafter.
Consider these two conditions:
- All text would normally be
black
, but Query A is unique and has color: red
.
- Since Query B is for larger viewports, it's text has the CSS
font-size: larger
.
Therefore, at a width of exactly 45em, we'd get big and red text. What would be the best solution to avoid this?
I see two possibilities:
Re-declare the text to have color: black
in Query B, but then you're managing two declarations if you choose to change the color
in the future. (Of course, not such a problem with this single line of code, but imagine there's a lot of other declarations and selectors.)
Avoid overlap by using pixel values like max-width: 799px
and min-width: 800px
, but then you're using pixels — I guess they could be 49.9375em and 50em, respectively. Though what if the default is no longer 16em and something gets rounded? And we're still not certain what happens at that gap. (A black hole that breaks the space-time continuum?)
Both have their drawbacks... any other ideas?
The only reliable way to create two mutually exclusive @media
blocks for any given media query is to use not
to negate it in one of the blocks. Unfortunately, this means repeating your media query once for each @media
block. So, instead of this for example:
@media (max-width: 49.9375em) {
body {
color: red;
}
}
@media (min-width: 50em) {
body {
font-size: larger;
}
}
You would have this:
/*
* Note: Media Queries 4 still requires 'not' to be followed by a
* media type (e.g. 'all' or 'screen') for reasons I cannot comprehend.
*/
@media not all and (min-width: 50em) {
body {
color: red;
}
}
@media (min-width: 50em) {
body {
font-size: larger;
}
}
Interactive jsFiddle demo
This is very effective at closing the gap with range media features like width
and height
since it essentially turns this into an either-or scenario. But, like your first two options, it isn't perfect: as mentioned, you have to repeat the same media query twice, and add not
to one of them. There is no if/else construct for @media
as described in Conditional Rules 3.
Although I mention this in my answer to your previous question:
From my experiments it would seem Safari on iOS rounds all fractional pixel values to ensure that either one of max-width: 799px
and min-width: 800px
will match, even if the viewport is really 799.5px (which apparently matches the former).
It should be noted, still, that I've noticed some quirks when it comes to rounding. That said, I haven't been able to find a fractional value that would evade both media queries and end up not receiving styles from either set of rules (which, by the way, is the worst that can happen, so don't worry about potentially creating a space-time rift). That must mean browsers — at least, Safari as I've tested — do a reasonable job of ensuring they satisfy media queries even if you have values that differ (by exactly 1 CSS pixel).
When it comes to units with larger gaps that can be observed on desktop browsers, though, like ems, there is a much larger margin of error. For example, one comment suggests using 49.99999em instead of something more arbitrary than 49.9375em, but apparently there is a difference, at least with a default font size of 16px.
I simplified your code, changed the media queries to use decimal values, and put the code in jsFiddle:
@media (max-width: 49.9375em) {
body {
color: red;
}
}
@media (min-width: 50em) {
body {
font-size: larger;
}
}
If you resize the Result pane to exactly 800 pixels (the text will update to guide you along), you actually end up with different results depending on whether @media (max-width: 49.9375em)
is used, or @media (max-width: 49.99999em)
is used (I was surprised by this too)...
Either way, you're right: option 2 has its drawbacks too. I'm not particularly fond of it, to be honest, because I wouldn't want to crack my head over device and user agent quirks which are out of my control. If you're like me, I suppose it would be better to go through the inconvenience of redeclaring your rules at the cost (?) of being more vigilant around your code, as that's at least still within your control as an author.
For me, the best way is to keep a gap of 0.01em:
@media (min-width: 20em) and (max-width: 44.99em) {
body { color: red; } /* supposed to be unique for this width */
}
@media (min-width: 45em) {
body { font-size: larger; } /* because viewport is bigger */
}
I recommend you to read this article for the details and the comparison of the different solutions to prevent media query overlapping.
Cheers,
Thomas.