Media Query Ranges

The Media Queries Level 4 specification introduced a new range syntax for media queries.

@media (480px <= width < 768px)

This is particularly useful because min-* and max-* media queries have always been inclusive of their boundary values.

A common bug that can be hard to reproduce is when pre-set media queries overlap at the boundaries.

@media (max-width: 768px) {
  .example {
    padding: 10px;
    width: 100%;
  }
}

@media (min-width: 768px) {
  .example {
    // uh oh! Both the margin and the padding will apply
    // when the browser is exactly 768px wide 😱
    margin: 15px;
    width: 200px;
  }
}

The classic solution is to make sure that the boundary values differ by 1px.

@media (max-width: 767px) {...}
@media (min-width: 768px) {...}

This works fine for units in px, but it can be problematic for values using em.

@media (max-width: 29em) {...}
@media (min-width: 30em) {...} //sizable gap

Since em units can be modified by the user by way of browser font-size settings, I've typically used 0.001 as my "epsilon" value to make sure that the em values are less than 1px apart, while still not causing an overlapping range:

@media (max-width: 29.999em) {...}
@media (min-width: 30em) {...}

Range syntax for media queries simplifies this by allowing both inclusive and exclusive bounds for min-* and max-* boundaries:

@media (width < 30em) {...}
@media (width <= 30em) {...}

Unfortunately, range media queries have almost no browser support at this time.

Fortunately, there's another way to handle exclusive bounds on media queries that does have cross-browser support. This alternative method involves two media query features.

Negated Media Queries

The first media query feature that's needed for handling non-overlapping media query ranges is the not media query operator.

The not operator is somewhat quirky, and generally doesn't behave how you might expect if you're familiar with negation in other programming languages.

The not operator negates the entire media query it's applied to. For example not screen and (min-width: 480px) matches non-screen devices that have a width less than 480px. The logic would be better expressed as not (screen and (min-width: 480px)), even though it otherwise might look like (not screen) and (min-width: 480px).

What's more, the not operator applies only to a single media query, even if multiple are included in a media query list in the @media declaration.

As an example, @media not screen, (min-width: 480px) is better represented as (not screen) or (min-width: 480px), even though it might seem like it ought to be applied as not (screen or (min-width: 480px) given how not behaves with and.

Furthermore, the not operator requires a media type which is redundant and verbose. Instead of being able to use @media not (min-width: 480px) you'll need to use @media not all and (min-width: 480px).

With all those caveats it might be difficult to see how negated media queries might be useful for media query ranges. The advantage of a negated range is that it turns inclusive min-* and max-* boundaries...

Unlabeled line graph of "greater than or equal to"

...to exclusive max-* and min-* boundaries respectively.

Unlabeled line graph of "less than"

@media not all and (max-width: 30em) {...} and @media (max-width: 30em) {...} produce a seamless range with the first media query representing widths less than 30em and the second media query representing widths greater than or equal to 30em.

While this is helpful, we still can't produce a single media query with upper and lower bounds where one is inclusive and the other exclusive. In order to do that, we need another feature...

Nested Media Queries

The second media query feature that's needed for handling non-overlapping media query ranges is media query nesting.

By using nested media queries, one @media declaration can apply with a second @media declaration that also applies within the first one. As they're separate declarations, the notoperator only applies to one, and they still combine as though they'd been joined with and:

@media screen {
  @media (min-width: 30em) {
    // this only applies to screens with a width greater than or equal to 30em (480px)
  }
}

Nesting has pretty good browser support with the notable exception of IE11. At the time of writing we're mere months away from Microsoft dropping support for IE.

All Together Now

Combining these two strategies we can build media query ranges with one inclusive bound and one exclusive bound.

If we want to include the lower bounds, use min-width for both bounds:

@media (min-width: 30em) {
  @media not all and (min-width: 64em) {
    .example { … }
  }
}

This is equivalent to

@media (30em <= width < 64em) {
  .example { … }
}

If we want to include the upper bounds, use max-width for both bounds:

@media not all and (max-width: 30em) {
  @media (max-width: 64em) {
    .example { … }
  }
}

This is equivalent to

@media (30em < width <= 64em) {
  .example { … }
}

tl;dr:

If you've been struggling with overlapping bounds on media query ranges, you can start using nested negated media queries today by using a pattern of

@media (min-width: [lower bound]) {
  @media not all and (min-width: [upper bound]) {
    .example { … }
  }
}

and maybe in a couple years you can simplify this to

@media ([lower bound] <= width < [upper bound]) {
  .example { … }
}