CodyHouse Framework + Components are featured on Product Hunt! Join the discussion →

An approach to responsive layouts based on CSS custom properties and em units

How to set smart variables that control the responsiveness of your web project.

The CodyHouse Framework relies on CSS variables and em units for a reason: when combined, they allow you to set (smart) responsive rules. These rules represent global controls: editing them means affecting the whole responsive scale, with (almost) no need to set media queries on a component level.

πŸ‘‹ First time you hear about the CodyHouse Framework?

Responsive typography #

Let's start from typography! In the πŸ“ custom-style/_typography.scss file of the CodyHouse framework, we set the type scale (πŸ‘‹ pssst...we have a tool to generate custom type scales). The reason why a modular scale is useful when applied to anything in a design system is that it generates a set of harmonious values, as opposed to setting each value independently.

To control the scale, we’ve defined two variables: the --text-base-size and the --β€Štext-scale-ratio. The first one is the body font size, while the second one is the ratio used to generate the scale. The default value of --text-base-size is 1em.

When applied to the <body>, 1em equals to 16px in most modern browsers. Since our framework is mobile-first, we're setting the body text size equal to 16px on smaller screens.

Here's the type scale:

:root {
  /* body font size */
  --text-base-size: 1em;

  /* type scale */
  --text-scale-ratio: 1.2;
  --text-xs: calc((1em / var(--text-scale-ratio)) / var(--text-scale-ratio));
  --text-sm: calc(var(--text-xs) * var(--text-scale-ratio));
  --text-md: calc(var(--text-sm) * var(--text-scale-ratio) * var(--text-scale-ratio));
  --text-lg: calc(var(--text-md) * var(--text-scale-ratio));
  --text-xl: calc(var(--text-lg) * var(--text-scale-ratio));
  --text-xxl: calc(var(--text-xl) * var(--text-scale-ratio));
  --text-xxxl: calc(var(--text-xxl) * var(--text-scale-ratio));
}

body {
  font-size: var(--text-base-size);
}

Note that in defining each text size variable we multiply 1em by the --text-scale-ratio. That 1em is not the --text-base-size value. You could set a --text-base-size different from 1em (while you shouldn’t change the 1m in the calc() function).

Since the Em unit is a relative unit equal to the current font size, if we update the --text-base-size variable at a specific media query, we update the font-size of the body, and, with a cascade effect, all the text size variables. The whole typography is affected.

@supports(--css: variables) {
  :root {
    @include breakpoint(md) {
      --text-base-size: 1.25em;
    }
  }
}
responsive-typography

The paragraph elements inherit the base font size, while we can set a specific font size for each heading element. Besides, in our framework we create utility classes in case, for example, we want to apply the --text-xxl font size to an element that is not an <h1>.

body {
  font-size: var(--text-base-size);
}

.text-xxxl {
  font-size: var(--text-xxxl);
}

h1, .text-xxl {
  font-size: var(--text-xxl);
}

h2, .text-xl {
  font-size: var(--text-xl);
}

h3, .text-lg {
  font-size: var(--text-lg);
}

h4, .text-md {
  font-size: var(--text-md);
}

.text-sm, small {
  font-size: var(--text-sm);
}

.text-xs {
  font-size: var(--text-xs);
}

Why including the type scale in your CSS? In one word: control.

Say we want to increase the body font size at a specific breakpoint; for example, we increase the --text-base-size to 1.25em past 1024px. The heading elements are all affected by the change (the 1em in the calc() function is no longer ~16px, but ~20px); therefore they all become bigger. Let’s suppose we feel like increasing the size of the <h1> element even more. How do we do that?

One option would be increasing the --text-base-size value, but it’s not ideal if I want to target only the heading elements, preserving the size of the body text. Here’s the advantage of storing the --text-scale-ratio in a variable. We can edit it and affect everything but the body text:

@supports(--css: variables) {
  :root {
    @include breakpoint(md) {
      --text-scale-ratio: 1.25;
    }
  }
}

With this technique, you can manage the size of all your text elements by editing only two variables. Besides, you can take advantage of the Em unit and modify all margins, paddings, and spacing as a consequence of editing the --text-base-size at a root level.

Responsive Typography
Responsive typography using CSS custom properties

Responsive spacing #

Similarly to how we did with typography, in our framework we include the spacing scale:

:root {
  --space-unit:  1em;
  --space-xxxxs: calc(0.125 * var(--space-unit));
  --space-xxxs:  calc(0.25 * var(--space-unit));
  --space-xxs:   calc(0.375 * var(--space-unit));
  --space-xs:    calc(0.5 * var(--space-unit));
  --space-sm:    calc(0.75 * var(--space-unit));
  --space-md:    calc(1.25 * var(--space-unit));
  --space-lg:    calc(2 * var(--space-unit));
  --space-xl:    calc(3.25 * var(--space-unit));
  --space-xxl:   calc(5.25 * var(--space-unit));
  --space-xxxl:  calc(8.5 * var(--space-unit));
  --space-xxxxl: calc(13.75 * var(--space-unit));
}

The --space-unit is equal to 1em, while the modular scale is based on the Fibonacci sequence (with a small tweak).

By setting the --space-unit equal to 1em, we're creating a bond between typography and spacing. In the previous chapter, we explained how the --text-base-size variable controls the whole type system. If you increase its value at a specific breakpoint, all the text size variables change accordingly.

@supports(--css: variables) {
  :root {
    @include breakpoint(md) {
      --text-base-size: 1.25em;
    }
  }
}

Since the spacing unit is equal to 1em, and all other spacing values are multipliers of the unit value, when we update the --text-base-size variable, we affect the spacing as well!

Here's the effect of updating a single CSS custom property:

Responsive spacing
Responsive spacing using em units

No additional media queries needed so far! All you have to do is using the spacing variables to set paddings and margins on a component level:

.header__top {
  background: var(--color-contrast-higher);
  padding: var(--space-sm);
  text-align: center;

  a {
    color: var(--color-white);
    @include fontSmooth;
  }
}

.header__main {
  border-bottom: 1px solid var(--color-contrast-low);
  padding-top: var(--space-sm);
  padding-bottom: var(--space-sm);
  background: var(--color-bg);
}

.header__nav {
  ul {
    display: flex;
  }

  li {
    margin-right: var(--space-md);

    &:last-child {
      margin-right: 0;
    }
  }
}

What if you want to update all spacing values at once, without having to change the --text-base-size variable? You can update the --space-unit variable:

:root {
  --space-unit:  1em;
  --space-xxxxs: calc(0.125 * var(--space-unit)); 
  --space-xxxs:  calc(0.25 * var(--space-unit));
  --space-xxs:   calc(0.375 * var(--space-unit));
  --space-xs:    calc(0.5 * var(--space-unit));
  --space-sm:    calc(0.75 * var(--space-unit));
  --space-md:    calc(1.25 * var(--space-unit));
  --space-lg:    calc(2 * var(--space-unit));
  --space-xl:    calc(3.25 * var(--space-unit));
  --space-xxl:   calc(5.25 * var(--space-unit));
  --space-xxxl:  calc(8.5 * var(--space-unit));
  --space-xxxxl: calc(13.75 * var(--space-unit));
}

@supports(--css: variables) {
  :root {
    @include breakpoint(md) {
      --space-unit:  1.25em;
    }
  }
}

Conclusion #

It is true that by embracing a method like this one you lose some of your “visual” control, but it’s in favor of simplicity and maintainability. When all your components are connected (thanks to relative units), making changes becomes a pain-free process (you know what I mean πŸ˜…). Accepting that we shouldn't control every single pixel of the page using plenty of media queries is the way that leads to simplifying the process of managing our web projects.

Project duplicated.