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.