Want CSS variables in media query declarations? Try this!

-

Published

Updated

If you're like me, you've probably been stretching CSS variables / custom properties to their limits while building your own design systems. But this "silver bullet" can lead to a nasty roadblock: you can't use them in media query declarations.

To clarify, this is the behavior you might want:

:root {
	--mobile-breakpoint: 600px;
	--tablet-breakpoint: 900px;
}

@media(max-width: var(--mobile-breakpoint)) {
	.obligatory-hamburger-menu { visibility: visible }
}

...which sadly won't work.

Why var() can't work in a @media declaration

This may seem confusing at first. If our variables are declared at the :root of the page, why can't a media query access them?

Well, it comes down to what the :root selector actually means: the root element of the HTML document. Unlike CSS selectors, media queries aren't attached to HTML elements at all; they belong to the global scope.

If this seems confusing, let's consider a CSS variable like this one on the body element:

body {
	--mobile-breakpoint: 600px;
}
@media(max-width: var(--mobile-breakpoint)) { /* ... */ }

Since our @media query is global, it cannot know about this --mobile-breakpoint variable. This extends to the :root selector as well.

SASS / SCSS can make this even more confusing by allowing @media blocks to be nested inside of other rulesets. Don't be fooled! When your SASS gets compiled, those media queries will be hoisted to the base of your CSS document.

For SASS and SCSS users: use SASS variables for media queries

If you're using SASS or SCSS, I recommend falling back to SASS variables for media queries and using CSS variables everywhere else.

This is because, unlike CSS variables, SASS variables are replaced at build-time with the values you set. This allows the SASS compiler to find-and-replace all instances of a variable, including variables referenced by @media query declarations.

@media (min-width: $mobile-breakpoint) { /* ... */ }
/* compiled output: */
@media (min-width: 600px) { /* ... */ }

Build-time replacement means you won't be able to dynamically update the value of a SASS variable from JavaScript. Still, you can use SASS variables to define static values in a design system like a $mobile-breakpoint.

Note that you'll need to fall back to Sass for math calculations as well, instead of using the dynamic calc() function:

/* ✅ good */
@media(max-width: $mobile-breakpoint + 50px) { /* ... */ }
/* ❌ bad */
@media(max-width: calc($mobile-breakpoint + 50px)) { /* ... */ }

For everyone else: use the incoming CSS environment variables spec

The W3C community isn't happy about the limitations of CSS variables either. Their proposal for environment variables addresses this very issue, letting you declare static variables that can be read from @media queries.

Browsers have followed this level 1 spec to let you read environment variables via the env() function. Similar to var(), you can call env() to read a variable's value like so:

@media (min-width: env(...)) { /* ... */ }

Before you go off and use it, there's a surprising show-stopper: you can't declare variables of your own yet 😅 Browser engines like webkit include a set of predefined environment variables for a different use case: setting the allowed area for notification popups when building web apps. Useful, but it doesn't address our @media query dilemma.

PostCSS polyfill for declaring environment variables

Luckily, there's a PostCSS plugin to let you define custom environment variables with a similar syntax to CSS variables. Install the plugin with npm like so:

npm install postcss postcss-env-function --save-dev

And define variables from your PostCSS configuration using the environmentVariables object:

postcssEnvFunction({
    environmentVariables: {
        '--mobile-breakpoint': '600px',
    },
})

Then, you can refer to variables from @media breakpoints using the env() function:

@media (min-width: env(--mobile-breakpoint)) { /* ... */ }

This has the same limitation as SASS variables: values are set at build-time and can't be updated dynamically. That also means dynamic functions like calc() won't work in @media queries. Still, this is a helpful workaround to reference environment variables using the familiar CSS variable syntax.

⚠️ Word of caution: This PostCSS plugin is a "stage 0" prototype, and it may not match browser behavior if environment variable declarations come to native CSS. Be sure to document your usage of this plugin and keep an eye on the spec for updates.

The whiteboardist newsletter

Occasional posts and learnings from a lead Astro maintainer.