CSS custom properties (a.k.a. CSS variables) are becoming more and more popular. They finally reached decent browser support and are slowly making their way into various production environments. The popularity of custom properties shouldn’t come as a surprise, because they can be really helpful in numerous use cases, including managing color palettes, customizing components, and theming. But CSS variables can also be really helpful when it comes to responsive design.
Article Series:
- Defining Variables and Breakpoints (This Post)
- Building a Flexible Grid System
Let’s consider an <article>
element with a heading and a paragraph inside:
<article class="post">
<h2 class="heading">Post's heading</h2>
<p class="paragraph">
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Laudantium numquam adipisci recusandae officiis dolore tenetur,
nisi, beatae praesentium, soluta ullam suscipit quas?
</p>
</article>
It’s a common scenario in such a case to change some sizes and dimensions depending on the viewport’s width. One way to accomplish this is by using media queries:
.post {
padding: 0.5rem 1rem;
margin: 0.5rem auto 1rem;
}
.heading {
font-size: 2rem;
}
@media (min-width: 576px) {
.post {
padding: 1rem 2rem;
margin: 1rem auto 2rem;
}
.heading {
font-size: 3rem;
}
}
See the Pen
#1 Building responsive features with CSS custom properties by Mikołaj (@mikolajdobrucki)
on CodePen.
Such an approach gives us an easy way to control CSS properties on different screen sizes. However, it may be hard to maintain as the complexity of a project grows. When using media queries, keeping code readable and DRY at the same time quite often turns out to be challenging.
The most common challenges when scaling this pattern include:
- Repeated selectors: Apart from bloating code with multiple declarations, it also makes future refactoring more difficult, e.g. every time a class name changes it requires remembering to update it in multiple places.
- Repeated properties: Notice that when overwriting CSS rules within media queries, it requires repeating the entire declaration (e.g.
font-size: 3rem;
) even though it’s just the value (3rem
) that actually changes. - Repeated media queries: To keep responsive styles contextual, it’s a common practice to include the same media queries in multiple places, close to the styles they override. Unfortunately, it not only makes code heavier, but also might make breakpoints much harder to maintain. On the other hand, keeping all responsive styles in one place, away from their original declarations, may be very confusing: we end up with multiple references to the same elements sitting in completely different places.
We can argue that repeated declarations and queries shouldn’t be such a big deal with proper file compression enabled, at least as long as we’re referring to performance. We can also merge multiple queries and optimize your code with post-processing tools. But wouldn’t it be easier to avoid these issues altogether?
There’s a lot of ways to avoid the issues listed above. One of them, that we will explore in this article, is to use CSS custom properties.
Using CSS variables for property values
There are plenty of amazing articles on the web explaining the concept of CSS custom properties. If you haven’t got chance to get familiar with them yet, I would recommend starting with one of the beginner articles on this topic such as this awesome piece by Serg Hospodarets as we are not going to get into details of the basic usage in this article.
The most common way of utilizing CSS custom properties in responsive design is to use variables to store values that change inside of media queries. To accomplish this, declare a variable that holds a value that is supposed to change, and then reassign it inside of a media query:
:root {
--responsive-padding: 1rem;
}
@media (min-width: 576px) {
:root {
--responsive-padding: 2rem;
}
}
.foo {
padding: var(--responsive-padding);
}
Assigning variables to the :root
selector is not always a good idea. Same as in JavaScript, having many global variables is considered a bad practice. In real life, try to declare the custom properties in the scope they will actually be used.
This way, we are avoiding multiple rules of the .foo
class. We are also separating the logic (changing values) from the actual designs (CSS declarations). Adapting this approach in our example from above gives us the following CSS:
.post {
--post-vertical-padding: 0.5rem;
--post-horizontal-padding: 1rem;
--post-top-margin: 0.5rem;
--post-bottom-margin: 1rem;
--heading-font-size: 2rem;
}
@media (min-width: 576px) {
.post {
--post-vertical-padding: 1rem;
--post-horizontal-padding: 2rem;
--post-top-margin: 1rem;
--post-bottom-margin: 2rem;
--heading-font-size: 3rem;
}
}
.post {
padding: var(--post-vertical-padding) var(--post-horizontal-padding);
margin: var(--post-top-margin) auto var(--post-bottom-margin);
}
.heading {
font-size: var(--heading-font-size);
}
See the Pen
#2 Building responsive features with CSS custom properties by Mikołaj (@mikolajdobrucki)
on CodePen.
Notice that the use of variables in shorthand properties (e.g. padding
, margin
or font
) allow some very interesting repercussions. As custom properties may hold almost any value (more on this later), even an empty string, it’s unclear how the value of a shorthand property will be separated out into longhand properties that are used in the cascade later. For example, the auto
used in the margin
property above may turn out to be a top-and-bottom margin, a left-and-right margin, a top margin, a right margin, a bottom margin or a left margin — it all depends on the values of the custom properties around.
It’s questionable whether the code looks cleaner than the one from the previous example, but on a larger scale, it’s definitely more maintainable. Let’s try to simplify this code a bit now.
Notice that some values are repeated here. What if we try to merge duplicate variables together? Let’s consider the following alteration:
:root {
--small-spacing: 0.5rem;
--large-spacing: 1rem;
--large-font-size: 2rem;
}
@media (min-width: 576px) {
:root {
--small-spacing: 1rem;
--large-spacing: 2rem;
--large-font-size: 3rem;
}
}
.post {
padding: var(--small-spacing) var(--large-spacing);
margin: var(--small-spacing) auto var(--large-spacing);
}
.heading {
font-size: var(--large-font-size);
}
See the Pen
#3 Building responsive features with CSS custom properties by Mikołaj (@mikolajdobrucki)
on CodePen.
It looks cleaner but is it actually better? Not necessarily. For the sake of flexibility and readability, this may not be the right solution in every case. We definitely shouldn’t merge some variables just because they accidentally turned out to hold the same values. Sometimes, as long as we’re doing this as a part of a well thought out system, it may help us simplify things and preserve consistency across the project. However, in other cases, such a manner may quickly prove to be confusing and problematic. Now, let’s take a look at yet another way we can approach this code.
Using CSS variables as multipliers
CSS custom properties are a fairly new feature to the modern web. One of the other awesome features that rolled out in the last years is the calc()
function. It lets us perform real math operations in live CSS. In terms of the browser support, it’s supported in all browsers that support CSS custom properties.
calc()
tends to play very nicely with CSS variables, making them even more powerful. This means we can both use calc()
inside custom properties and custom properties inside calc()
!
For example, the following CSS is perfectly valid:
:root {
--size: 2;
}
.foo {
--padding: calc(var(--size) * 1rem); /* 2 × 1rem = 2rem */
padding: calc(var(--padding) * 2); /* 2rem × 2 = 4rem */
}
Why does this matter to us and our responsive designs? It means that we can use a calc()
function to alter CSS custom properties inside media queries. Let’s say we have a padding that should have a value of 5px
on mobile and 10px
on desktop. Instead of declaring this property two times, we can assign a variable to it and multiply it by two on larger screens:
:root {
--padding: 1rem;
--foo-padding: var(--padding);
}
@media (min-width: 576px) {
:root {
--foo-padding: calc(var(--padding) * 2);
}
}
.foo {
padding: var(--foo-padding);
}
Looks fine, however all the values (--padding
, calc(--padding * 2)
) are away from their declaration (padding
). The syntax may also be pretty confusing with two different padding variables (--padding
and --foo-padding
) and an unclear relationship between them.
To make things a bit clearer, let’s try to code it the other way around:
:root {
--multiplier: 1;
}
@media (min-width: 576px) {
:root {
--multiplier: 2;
}
}
.foo {
padding: calc(1rem * var(--multiplier));
}
This way, we accomplished the same computed output with much cleaner code! So, instead of using a variable for an initial value of the property (1rem
), a variable was used to store a multiplier (1
on small screens and 2
on larger screens). It also allows us to use the --multiplier
variable in other declarations. Let’s apply this technique to paddings and margins in our previous snippet:
:root {
--multiplier: 1;
}
@media (min-width: 576px) {
:root {
--multiplier: 2;
}
}
.post {
padding: calc(.5rem * var(--multiplier))
calc(1rem * var(--multiplier));
margin: calc(.5rem * var(--multiplier))
auto
calc(1rem * var(--multiplier));
}
Now, let’s try to implement the same approach with typography. First, we’ll add another heading to our designs:
<h1 class="heading-large">My Blog</h1>
<article class="post">
<h2 class="heading-medium">Post's heading</h2>
<p class="paragraph">
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
Laudantium numquam adipisci recusandae officiis dolore tenetur,
nisi, beatae praesentium, soluta ullam suscipit quas?
</p>
</article>
With multiple text styles in place, we can use a variable to control their sizes too:
:root {
--headings-multiplier: 1;
}
@media (min-width: 576px) {
:root {
--headings-multiplier: 3 / 2;
}
}
.heading-medium {
font-size: calc(2rem * var(--headings-multiplier))
}
.heading-large {
font-size: calc(3rem * var(--headings-multiplier))
}
You may have noticed that 3 / 2
is not a valid CSS value at all. Why does it not cause an error then? The reason is that the syntax for CSS variables is extremely forgiving, which means almost anything can be assigned to a variable, even if it’s not a valid CSS value for any existing CSS property. Declared CSS custom properties are left almost entirely un-evaluated until they are computed by a user agent in certain declarations. So, once a variable is used in a value of some property, this value will turn valid or invalid at the computed-value time.
Oh, and another note about that last note: in case you’re wondering, I used a value of 3 / 2
simply to make a point. In real life, it would make more sense to write 1.5
instead to make the code more readable.
Now, let’s take a look at the finished live example combining everything that we discussed above:
See the Pen
#4 Building responsive features with CSS custom properties by Mikołaj (@mikolajdobrucki)
on CodePen.
Again, I would never advocate for combining calc()
with custom properties to make the code more concise as a general rule. But I can definitely imagine scenarios in which it helps to keep code more organized and maintainable. This approach also allows the weight of CSS to be significantly reduced, when it’s used wisely.
In terms of readability, we can consider it more readable once the underlying rule is understood. It helps to explain the logic and relations between values. On the other hand, some may see it as less readable, because it’s tough to instantly read what a property holds as a value without first doing the math. Also, using too many variables and calc()
functions at once may unnecessarily obscure code and make it harder to understand, especially for juniors and front-end developers who are not focused on CSS.
Conclusion
Summing up, there’s a lot of ways to use CSS custom properties in responsive design, definitely not limited to the examples shown above. CSS variables can be used simply to separate the values from the designs. They can also be taken a step further and be combined with some math. None of the presented approaches is better nor worse than the others. The sensibility of using them depends on the case and context.
Now that you know how CSS custom properties can be used in responsive design, I hope you will find a way to introduce them in your own workflow. Next up, we’re going to look at approaches for using them in reusable components and modules, so let’s check that out.
Very useful, thank you. Another one of these things I knew about but didn’t consider this way ;)
If you want to get really fancy with it, you can include viewport values (vw and vh) in your calculations to create responsive layouts without media queries. But you have to be careful with that, as there’s no min or max value for things like padding and font-size. There are SASS mixins that will generate responsive scaling properties based on viewport width as well as min/max media queries, but that can get a bit messy. Your best bet is to use those values sparingly and to test your layout at extremely large and small sizes.
The trick to making this maintainable is to have very explicit and meaningful CSS variable names. So rather than
--big-font
you can have--header-h1-font-size
or--article-h2-font-size
.Extending on this principle if you are using a CSS declaration that has lots of numbers in it, e.g. for margins or CSS grid widths then you can place all of this logic in your CSS variable declarations at the top of your document, those declarations referring to other CSS variables.
So rather than the way the padding is specified here for the ‘.post’ class, you could have ‘padding: var(–post-padding);’ and then have –post-padding defined in the top with the details of that just specified in the root element and the multiplier being a few lines away.
Getting the naming convention right is the best way to get things maintainable. A lot of complexity is moved to CSS variables and by being strict on the naming convention you can sort your CSS variables and keep everything easy to understand.
Also vague terms such as ‘multiplier’ are not that useful, ‘–page-margin’ does what it says on the tin and is not a magic number.
If you are using some CSS compiler then keeping your CSS variables in a separate file helps, on smaller projects you can go with just the one file and split the view in your code editor so you can easily refer to your CSS variables as you delve through your code. Using calc is fine if it is all in the CSS variable block at the top of the page, in this way your CSS can have simple declarations for properties with the logic abstracted out.
If going with CSS variables and CSS Grid then you might as well move away from pixels, percentages and rems to ems and viewer units. There is no room for pixels in 2019, they are a bad way of thinking when screens have virtual pixels. So rather than 576 pixels you want 36 ems.
The cascade is also a thing so rather than clutter your document with untold class elements you can use the cascade and the magic of ems to get the paragraph text font size correct in relation to the heading in the article by just setting the properties on the article.
Very useful info on the subject. I like how the final example is done but I’m also seeing an issue in my work when I have project managers that ask what size something is and to “bump it up” a pt. Tracking down a specific value in a dynamic environment is always hard…. guess it’s a give-and-take situation.
If there was just a way to use custom variables inside media query declarations (such as
@media screen and (min-width: var(--breakpoint)) { ... }
) As far as I know this is not possible — do you have any idea on this subject?Unfortunately, that’s right (as far as I know). You can’t use custom properties within CSS at-rules such as @media. I’ve never been in need for such but I can definitely imagine scenarios where it would be useful.
Treating this as a progressive enhancement, you can always achieve it with JavaScript though.