CSS Custom Properties, Media Queries, And Responsive Design

One thing you can do with CSS custom properties that you can’t do with variables in preprocessors is redefine them and assign new values inside @media queries. It’s a useful ability to have and one more reason why you’d want to work with what are effectively native CSS variables.

The last few weeks I’ve been looking at custom properties. I began by walking through how to define and use them and last week I started to look at some simple examples to make your code more readable and to help with contextual styling.

Those examples didn’t take advantage of the dynamic capabilities of custom properties and I want to change that starting today. I want to show you how we can use custom properties inside @media queries in a way you can’t with variables in preprocessors like Sass or Less.

Changing Color Inside @media Queries

Let’s start with a simple example where I’ll change the background color of the body element based on screen width.

I defined two custom properties, one for the background-color (#fff) and one for text color (#000). I set both on :root so they’re effectively global variables.

I then used the var() function on the background-color and color properties of the body element in order to use the two custom properties.

1
2
3
4
5
6
7
8
9
:root {
 --body-background: #fff;
 --body-color: #000;
}

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

Imagine you want the color and background-color of the body to change as the viewer’s screen gets wider. Let’s flip the colors at 30em so we get white text on a black background and then at 60em, let’s change the background to dark green and use its complement, a light purple, as the text color.

Without custom properties, you’d set new values for both the background-color and color properties inside the appropriate @media queries. That would certainly accomplish the same goal. With custom properties we can instead redefine the custom properties directly.

1
2
3
4
5
6
7
8
9
10
11
12
13
@media (min-width: 30em) {
 :root {
   --body-background: #000; 
   --body-color: #fff;
 }
}

@media (min-width: 60em) {  
 :root {
   --body-background: #232; 
   --body-color: #dcd;
 }
}

You might think that wasn’t anything special and that it was no easier than setting new values on the two properties. You’re probably right with such a simple example. However, with more complex examples being able to redefine one or two custom properties will likely be easier than overriding all the CSS properties that rely on one or two preprocessor variables.

Layout Changes Inside @media Queries

We can use the same technique to help with responsive layouts. Let’s use flexbox for a component layout. Here I created four divs, each representing a grid cell of some kind. Perhaps these would all be included as part of a “what can you do on this site“ block of content somewhere on the home page of a site.

1
2
3
4
5
6
<body>
 <div>Cell One</div>
 <div>Cell Two</div>
 <div>Cell Three</div>
 <div>Cell Four</div>
</body>

In the CSS below, I created two custom properties, one that will become the base width of each grid cell and one that will become the base margin for each cell.

I set up the body element to be a flex container with items that wrap to a new line when available space doesn’t allow them all to fit on a single row. The four divs will be the flex-items inside the container.

1
2
3
4
5
6
7
8
9
:root {
 --cell-width: 100%;
 --cell-margin: 0.25em;
}

body {
 display: flex;
 flex-wrap: wrap;
}

The first few properties on the div set a background color, height, and a flex of 1. I then set each div as its own flex container in order to center the text inside both horizontally and vertically using justify-content and align-items.

The last two lines define a flex-basis (effectively the width of each cell) and the margin (the gutter between grid cells) in terms of the two custom properties defined inside :root. I did this using the CSS calc() function, which you’ll often find is very useful when combined with custom properties.

1
2
3
4
5
6
7
8
9
10
11
12
div {
 background: #ccc;
 height: 15em;
 flex: 1;

 display: flex;
 justify-content: center;
 align-items: center;

 flex-basis: calc(var(--cell-width) - (var(--cell-margin) * 2));
 margin: var(--cell-margin);
}

It makes sense to have each cell fill the entire width of a smartphone screen, but as the width of the screen increases we want more than one cell to display per row. As the screen width continues to increase we eventually want all four cells to display on a single row.

All we have to do to make that happen is adjust the values of the custom properties inside @media queries. The values for the flex-basis and the margin will adjust without any extra help.

1
2
3
4
5
6
7
8
9
10
11
12
13
@media (min-width: 37.5em) {
 :root {
   --cell-width: 50%;
   --cell-margin: 0.75em;
 }
}

@media (min-width: 60em) {
 :root {
   --cell-width: 25%;
   --cell-margin: 1em;
 }
}

As with the previous example, we don’t have to redo the layout code inside the @media queries as we would without custom properties. We only needed to change the value of the custom properties and let everything else adjust.

Now as the screen width changes, the value of the cell width and cell margin can change with it.

Philip Walton offers a similar example with a bit more detail showing code to make the same changes using preprocessor variables and custom properties.

Typographic Changes Inside @media Queries

Let’s continue with a couple more examples, this time adjusting type. I’ll begin with a variation of an example that adjusts the modular scale of the type.

If modular scale is new to you, the basic idea is you use it to create meaningful ratios of type sizes. For example, say your default font-size is 1em (16px). You know your h1 and h2 headings will be larger than 1em, but how much larger? If you base sizes on a modular scale you can create a certain harmony of type sizes.

For simplicity, let’s say you choose to use a modular scale of 1.5. You’d multiply 1em by 1.5 to get 1.5em and then multiply 1.5em by 1.5 to get 2.25em and so on. You would also do the math in reverse to find sizes smaller than 1em.

  • 0.444em, 0.667em, 1em, 1.5em, 2.25em, 3.375em, and so on
  • 7.104px, 10.672px, 16px, 24px, 36px, 54px, and so on

Now when it’s time to set the size of an h2 you can choose 1.5em (24px) and then use 2.25em (36px) for your h1.

The problem is while a modular scale of 1.5 might work well on wider screens with lots of space, it probably doesn’t work so well on smaller screens because of their limited space. A modular scale of 1.2 might work better on smartphone screens and maybe 1.33 would be ideal for smaller tablets.

The solution is to set up several typographic properties based on a modular scale value and then adjust the value of the modular scale inside @media queries.

Let’s set up something simple. Here we have a paragraph and two headings. The paragraph will be the default font-size, the h2 will be one level larger on the modular scale, and the h1 will be one additional level larger on the modular scale.

1
2
3
<h1>Heading level 1</h1>
<h2>Heading level 2</h2>
<p>Paragraph</p>

In the CSS we’ll define a couple of custom properties on the :root and then assign values to different CSS properties for the p, h2, and h1 elements in terms of these custom properties.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
:root {
  --base-font-size: 1em;
  --modular-scale: 1.2;
}

h1 {
  font-size: calc(var(--modular-scale) * 3 * var(--base-font-size));
}

h2 {
  font-size: calc(var(--modular-scale) * 2 * var(--base-font-size));
}

p {
  font-size: var(--base-font-size);
}

These values work well for small screens, but we need to adjust them for larger screens. Because everything is based on the modular scale all we need to do is redefine the –modular-scale custom property inside the appropriate @media queries and again let everything else adjust.

1
2
3
4
5
6
7
8
9
10
11
@media (min-width: 37.5em){
  :root{
    --modular-scale: 1.333
  }
}

@media (min-width: 60em){
  :root{
    --modular-scale: 1.414
  }
}

I hope you’re seeing the pattern running through these examples. You define one or more custom properties and set values on regular CSS properties in terms of the custom properties, sometimes with the help of the calc() function.

Inside the @media queries all you need to do is redefine the custom property and everything else adjusted on its own. It’s one way to make your type more responsive with custom properties, though there are others.

Once again, I kept the example on the simpler side, but imagine setting up more of your type this way. Maybe you set a –base-line-height and use that to calculate an assigned line-height. You might further set the line length in terms of –base-font-size and –base-line-height and be able to adjust the proportion of your fonts by redefining a couple of CSS variables inside different @media queries.

Closing Thoughts

Hopefully these examples have given you some ideas for how you might use custom properties in your responsive design work. The main advantage is that you can redefine the value of any custom property inside @media queries instead of having to rewrite a lot of CSS inside them. This helps to keep your @media queries cleaner and less verbose.

For this to work you typically need to make use of the calc() function so you can define different properties in terms of several custom properties or a combination of values you set and custom properties that will change.

Next week I want to continue with more examples and show how you can dynamically change custom property values outside of @media queries, both with and without the use of Javascript.

Download a free sample from my book, Design Fundamentals.

Leave a Reply

Your email address will not be published. Required fields are marked *