Responsive Designs and CSS Custom Properties: Defining Variables and Breakpoints

Avatar of Mikolaj Dobrucki
Mikolaj Dobrucki on (Updated on )

DigitalOcean provides cloud products for every stage of your journey. Get started with $200 in free credit!

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:

  1. Defining Variables and Breakpoints (This Post)
  2. 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.