Front-end education for the real world. Since 2018.





A workaround for using custom properties in media queries

Manuel Matuzović

Topic: CSS

I’m a big fan of custom properties and I use them almost everywhere. One of the places where I’d love to use them, but can’t, is with media queries. The following code won’t work in any browser.

Code language
css

:root {
  --breakpoint-s: 480px;
}

@media (min-width: var(--breakpoint-s)) {
  /* do something */
}

The Custom Properties specification explains:

The var() function can be used in place of any part of a value in any property on an element. The var() function can not be used as property names, selectors, or anything else besides property values. (Doing so usually produces invalid syntax, or else a value whose meaning has no connection to the variable.)

That’s unfortunate, but I recently found a workaround that I’d love to share with you. Instead of using a media query, you can use a container style query.

Code language
css

@property --inline-size-s {
  syntax: "<length-percentage>";
  inherits: true;
  initial-value: 100vi;
}

:root {
  --breakpoint-s: 48em;
  --inline-size-s: min(var(--breakpoint-s), 100vi);
}

body {
  background-color: var(--bg-color);

  --bg-color: oklch(0.94 0.01 99);

  @container style(--inline-size-s: var(--breakpoint-s)) {
    --bg-color: oklch(0.87 0.21 95.82);
  }
}

Let me guide you through the steps I took to come up with this solution.

Breakdownpermalink

The first step is to define a custom property for the breakpoint. Then, query the viewport’s width and check whether it matches or exceeds the breakpoint. As already teased with the title of this article, you have to use container style queries for that, rather than media queries. Container style queries enable you to check whether a container has a specific property and computed value assigned.

Code language
css

:root {
  --breakpoint-s: 48em;
  --inline-size-s: 100vw;
}

@container style(--inline-size-s: var(--breakpoint-s)) {
  body {
    --bg-color: oklch(0.87 0.21 95.82);
  }
}

That looks good but it doesn’t work for two reasons.

You’re comparing two strings: does “48em” match “100vi”? That, of course, will never be true. What you actually want to do is compare the computed values of two lengths. Using the @property at-rule you can define an explicit type for your custom properties.

From the specification for the min-width property, we know that its value can be a <length-percentage>, so you can use that for the syntax property, which defines the types. You also have to provide an initial value; 100vi is a good default for our wannabe media queries.

The property should also be inheritable, so you set inherits accordingly.

Code language
css

@property --inline-size-s {
  syntax: "<length-percentage>";
  inherits: true;
  initial-value: 100vw;
}

Your query now works, but only if the viewport width is exactly 48em (roughly 768px, depending on the user’s preferred root font size).

You want it to work at at least 48em, though. The problem there is that if the computed value of --inline-size-s exceeds 48em it can’t match your breakpoint --breakpoint-s. That’s why you want --inline-size-s to match the viewport width but only until it reaches 48em. You can use the min() function for that. It takes the smaller of the provided values.

Code language
css

:root {
  --breakpoint-s: 48em;
  --inline-size-s: min(var(--breakpoint-s), 100vi);
}

--inline-size-s matches 100vi as long as the computed value of 100vi is lower than 48em. Otherwise, it matches 48em.

Here’s the whole snippet.

Code language
css

@property --inline-size-s {
  syntax: "<length-percentage>";
  inherits: true;
  initial-value: 100vi;
}

:root {
  --breakpoint-s: 48em;
  --inline-size-s: min(var(--breakpoint-s), 100vi);
}

body {
  background-color: var(--bg-color);

  --bg-color: oklch(0.94 0.01 99);

  @container style(--inline-size-s: var(--breakpoint-s)) {
    --bg-color: oklch(0.87 0.21 95.82);
  }
}

Tip

Toggle the CSS panel to see the effect

See the Pen A workaround for using custom properties in media queries (Demo 1) by piccalilli (@piccalilli) on CodePen.

Performancepermalink

You may wonder if there are any performance issues. I haven’t tested this solution on a large scale, but I can’t imagine so. Unlike container size queries, which query the size of the query container’s principal box, container style queries only query computed values of its container. That is much safer and harmless in terms of performance, which is also one of the reasons why every element is a style container by default. In the following demo, you can even change the breakpoint on the fly using a range slider.

Downsidespermalink

That’s a great solution to a problem probably many of us had, but there are also some downsides.

Browser Support

Container style queries are currently not supported by Firefox, at the time of writing, but I’ve heard from trustworthy sources that they’re being prioritised. Unfortunately, there isn’t a great way to progressively enhance this solution because there’s no feature detection for at-rules in CSS. If you really want to make it work, read this post by Bramus.

Verbosity

That isn’t really an issue, but it’s annoying that for every breakpoint, you need two custom properties, and you have to register one of them.

Code language
css

@property --inline-size-s {
  syntax: "<length-percentage>";
  inherits: true;
  initial-value: 100vi;
}

@property --inline-size-m {
  syntax: "<length-percentage>";
  inherits: true;
  initial-value: 100vi;
}

:root {
  --breakpoint-s: 48em;
  --inline-size-s: min(var(--breakpoint-s), 100vi);

  --breakpoint-m: 64em;
  --inline-size-m: min(var(--breakpoint-m), 100vi);
}

That will change when browsers start supporting style ranges.

Code language
css

@container style(100vi <= --inline-size-s) {
  /* The rest of your CSS */
}

Container queries

With this solution, you can only query the viewport, not a container. If you write min(var(--breakpoint-s), 100cqi), the 100cqi doesn’t refer to the style container but to a parent container. So, the only way to make it work would be to wrap the style container in another inline-size container.

Wrapping uppermalink

I love this solution. Not just because it solves a problem I recently had, but also because it showcases the power of modern CSS features like @container, @property or min() really well.

Browser support may be a topic for you right now, but hopefully, it will soon be a thing of the past.

Enjoyed this article? You can support us by leaving a tip via Open Collective


Newsletter