CSS variable and :root

CSS variable and :root with media query

Traditional Approach

When defining custom styles, we traditionally rely on media queries to style the elements for different viewports. The styles among different viewport differs for font-size, margin and padding usually. Also, the layout between mobile and above would differ.

h1 {
  font-size: 2.5rem;
  line-height: 5rem;
}
h2 {
  font-size: 1.6rem;
  line-height: 3.2rem;
}

main,
aside {
  padding: 0.5rem;
}

@media screen and (min-width: 550px) {
  h1 {
    font-size: 3rem;
    line-height: 6rem;
  }
  h2 {
    font-size: 2rem;
    line-height: 4rem;
  }

  main,
  aside {
    padding: 1rem;
  }
}

Introducing :root

The :root CSS pseudo-class matches the root element of a tree representing the document.

NOTE: In HTML, :root represents the <html> element and is identical to the selector html, except that its specificity is higher.

:root {
  background: yellow;
}

Introducing CSS variables (Custom Properties)

Property names that are prefixed with --, like --font-size, represent custom properties that contain a value that can be used in other declarations using the var() function.

NOTE: Custom properties are scoped to the element(s) they are declared on.

We could define the CSS variable as follows:

h1 {
  --font-size: 3rem
  font-size: var(--font-size);
  line-height: calc(var(--font-size) * 2)
}

h2 {
  --font-size: 2rem
  font-size: var(--font-size);
  line-height: calc(var(--font-size) * 2)
}

For global variables, :root is where we define the CSS variables.

:root {
  --font-size-1: 3rem;
  --font-size-2: 2rem;
}

h1 {
  font-size: var(--font-size-1);
  line-height: calc(var(--font-size-1) * 2);
}

h2 {
  font-size: var(--font-size-2);
  line-height: calc(var(--font-size-2) * 2);
}

Using CSS variables and :root with media queries

Taking the first example here, we could do the same using CSS variables and :root. Here instead of defining media queries on element level, we are adding it to the variables that are used within elements:

:root {
  --font-size-1: 2.5rem;
  --font-size-2: 1.6rem;
  --padding-base: 0.5rem;
}

@media screen and (min-width: 550px) {
  :root {
    --font-size-1: 3rem;
    --font-size-2: 2rem;
    --padding-base: 1rem;
  }
}

h1 {
  font-size: var(--font-size-1);
  line-height: calc(var(--font-size-1) * 2);
}

h2 {
  font-size: var(--font-size-2);
  line-height: calc(var(--font-size-2) * 2);
}

main,
aside {
  padding: var(--padding-base);
}

Another example for layout styles

Say you want to style a container whose elements needs to be laid out in columns while viewed on larger screen, otherwise on mobile it should be laid out as rows.

<div class="tiles grid-box">
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
</div>

<div class="cards grid-box">
  <div class="card"></div>
  <div class="card"></div>
  <div class="card"></div>
  <div class="card"></div>
</div>

In the example above, you could have added styles as follows to the .grid-box class thats shared between these list item components .tiles and .cards.

.grid-box {
  display: grid;
  grid-template-columns: 1fr;
}

@media screen and (min-width: 550px) {
  .grid-box {
    grid-template-columns: 1fr 1fr;
  }
}

@media screen and (min-width: 992px) {
  .grid-box {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

Depending on your use case, you could do it in :root to make styles cleaner and simpler.

:root {
  --grid-template-columns: 1fr;
}

@media screen and (min-width: 550px) {
  :root {
    grid-template-columns: 1fr 1fr;
  }
}

@media screen and (min-width: 992px) {
  :root {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

.grid-box {
  display: grid;
  grid-template-columns: var(--grid-template-columns);
}

Let me know what style do you prefer and why.

CSS Variable scope

Remember that CSS Variables (custom properties) are scoped to the element(s) they are declared on. So, its not just :root on which variables can be defined globally. You could define variables for an element and its decendents. Here is an example:

<body>
  <div class="content">
    <div class="tiles">
      <div class="tile"></div>
      <div class="tile"></div>
      <div class="tile"></div>
      <div class="tile"></div>
      <div class="tile"></div>
      <div class="tile"></div>
    </div>
  </div>
</body>

Considering the above template, we can define few custom properties to be shared globally on :root. And then few custom properties that needs to be shared among many parts of the page, can be moved to the parent of those elements.

:root {
  --root-content-width: 96vw;
  --padding-base: 1rem;
  --padding-base-2x: calc(var(--padding-base) * 2);
}

/* Layout */
.content {
  --tiles-columns-count: 1;
  --tiles-columns-gap: 0px;
  --content-width: calc(var(--root-content-width) - var(--padding-base-2x));
  --tiles-width: calc(var(--content-width) - var(--tiles-columns-gap));
  --tile-width: calc(var(--tiles-width) / var(--tiles-columns-count));
}
@media screen and (min-width: 550px) {
  .content {
    --tiles-columns-count: 2;
    --tiles-columns-gap: var(--grid-column-gap);
  }
}
@media screen and (min-width: 992px) {
  .content {
    --tiles-columns-count: 3;
    --tiles-columns-gap: calc(2 * var(--grid-column-gap));
  }
}

.tiles {
  display: grid;
  column-gap: var(--grid-column-gap);
  row-gap: var(--padding-base-2x);
  grid-template-columns: repeat(var(--tiles-columns-count), var(--tile-width));
}