Building Resizeable Components with Relative CSS Units

The following is a guest post by Ahmad Shadeed. Ahmad has put together a ton of examples to show off how using relative units can benefit us. I think a lot of us think of units like em as being for font-size, which it is, but can also be for lots of other things, tying together typographic sizing and other visual elements.

We are living in a dynamic world, everything we do could be changed at anytime. We, as Front-End Developers should build our layouts in a dynamic approach.

In this article, we will explore a concept that lets us resize our components by using CSS relative units (%, em, or rem). Not just the type size, but all the UI in that component. We'll look at practical examples, pros and cons of the approach, and even a complete web page built out in this manner.

A simple example of proportional font sizes

There are three elements here:

  1. Subtitle
  2. Title
  3. Left Border

With HTML something like this:

<article class="post">
   <a href="#">
     <span class="post-category">Featured</span>
     <h2 class="post-title">Building Dynamic Components is Awesome</h2>
   </a>
</article>

I want everything to be proportional to each other, so that when things resize, they do so together:

Let's say we started in pixels:

.post {
  border-left: 4px solid #4a90e2;
}
.post-category {
  font-size: 14px;
  color: #7d7d7d;
}
.post-title {
  font-size: 36px;
  font-weight: bold;
  color: #4a90de;
}

The only way to resize the entire component proportionally, then, is to adjust the font-size of each element manually to match each other at a new size. Perhaps your client tells you they'd like to see that area 1.5× bigger, you'd have to adjust those sizes to 21px and 54px respectively.

To make things a bit easier to adjust, we could use percentage font sizing.

.post-category {
  font-size: 85%;
}
.post-title {
  font-size: 135%;
}

This is saying: the font-size should be 85% of its closest parent element with a defined font-size.

We could set that font-size on the parent element:

.post {
  font-size: 24px;

  /* 
    Child elements with % font sizes...

    85%
    0.85 * 24 = 20.4
    
    135%
    1.35 * 24 = 32.4
  */
}

The browser calculates those font sizes for you when you use percentages and you don't really need to care what the final values are. It's just a proportion.

We could do the exact same thing with em values if we wanted. With font-size, percentage and em are kinda the same thing.

.post-category {
  font-size: 85%;
  /* the same as */
  font-size: 0.85em;
}
.post-title {
  font-size: 135%;
  /* the same as */
  font-size: 1.35em;
}

When we use em for values other than font-size, the computed pixel value it represents is still based on font-size. This is unlike, say, percentages, where a percentage width is based on the width of the parent, not the font-size!

For instance, if we set:

.post {
  font-size: 24px;
  border-left: 0.25em solid #4a90e2;
}

The border-left-width will compute to 6px.

In the following interactive demo, the slider changes the font-size on both components. The first everything set in pixels: font-size, border, margin, and padding. The second sets all those things in ems:

See the demo.

Proportional scaling for everything!

Proportional Buttons

Sometimes we need variations of a button. Perhaps a bigger version to emphasize a more important call to action. We can benefit from em units by using it for the padding, that way we can easily increase the size of the button through both font-size and padding.

<button class="button">Save Settings</button>

<button class="button button--medium">Save Settings</button>

<button class="button button--large">Save Settings</button>

If we set all those variations in pixels, we'd have something like:

.button {
  font-size: 16px;
  padding: 10px 16px;
  border-radius: 3px;
}

.button--medium {
  font-size: 24px;
  padding: 15px 24px;
  border-radius: 4px;
}

.button--large {
  font-size: 32px;
  padding: 20px 32px;
  border-radius: 5px;
}

Instead, we could combine percentage and em values to make things proportional, including the border-radius!

.button {
  font-size: 1em; /* Let's say this computes to 16px */
  padding: 0.625em 1em; /* 0.1875 * 16 = 10px */
  border-radius: 0.1875em; /* 0.1875 * 16 = 3px */
}

.button--medium {
  font-size: 130%;
}

.button--large {
  font-size: 160%;
}

Check them all scale in proportion:

See the demo.

Proportional Width/Height of Images

Here's an example where we need to keep an image avatar a bit bigger than the byline and publication date. Notice the blue highlight. Its height should change as we scale the font-size.

The HTML could be like:

<div class="bio">
  <img src="author.jpg" alt="Photo of author Ahmad Shadeed">
  <div class="bio__meta">
    <h3><b>By:</b> Ahmad Shadeed</h3>
    <time>Posted on August 5, 2016</time>
  </div>
</div>

And we'd set not only the font sizes in em values but also the width and height of the image. Make sure the image itself is big enough to scale without losing too much quality!

.bio h3 {
  font-size: 1em;
}
.bio time {
  font-size: 0.875em;
}
.bio img {
  width: 3.125em;
  height: 3.125em;
}

A scaling "border"

Another property that is happy to be set in em values: box-shadow.

We already know borders can be scaleable with em values. Here, we'll set the "height" of an inset box-shadow with an em value so it scales with the size of the text.

.headline {
  box-shadow: inset 0 -0.25em 0 0 #e7e7e7;
}

See the demo.

Note: we could have also pulled this off with CSS gradients with hard color stops, as those stops could be set at em values as well!

Making Room for Icons

Say we have a decorative <blockquote> with a custom icon in the upper left. We should account for the scenario when the font size changes. Relative units can help again here.

<blockquote class="quote">
  <p>
    <span>
      Building dynamic web components using modular design concepts is awesome. 
      <em>- Ahmad Shadeed</em>
    </span>
  </p>
</blockquote>

We'll set everything with relative units like we've touched on already. The decorative icon we will place with an SVG image applied via a pseudo element. We'll position that pseudo element absolutely, again with relative positioning and sizing, leaving enough space for it (with relative padding) on the parent element.

.quote {
  position: relative;
  padding: 1.5em 2em;
  padding-left: 4.5em;
  border-radius: 0.3125em;
}
.quote p {
  font-size: 2em;
}
.quote span {
  box-shadow: inset 0 -0.25em 0 0 rgba(255, 255, 255, 0.4);
}
.quote:before {
  content: "";
  position: absolute;
  top: 2.125em;
  left: 1.875em;
  background: url("quotes.svg") no-repeat;
  height: 1.875em;
  width: 1.875em;
}

With that in place, if we change the font size, everything scales fine:

Notice how everything scales up proportionally, just as it would if we grouped these elements and drag-scaled it up in a design application!

If we used pixel values instead, things wouldn't have scaled quite so nicely. The icon, in particular, could get awkwardly close to the text, or awkwardly far away:

Interactive example:

See the demo.

Figure with a caption

Imagine a photo and caption arranged like this:

We can make much of this design based on the font-size, like amount it is offset to the left and top, the padding, and even the shadow.

<figure class="figure">
  <img src="sunrise.jpg" alt="Sunrise">
  <figcaption>The feeling you got from watching the sunrise is amazing.</figcaption>
</figure>
.figure figcaption {
  position: absolute;
  top: 1.25em;
  left: -1.875em;
  right: 0;
  padding: 1em;
  box-shadow: -0.3125em 0.3125em 0 0 rgba(0, 0, 0, 0.15);
  font-size: 1.75em;
}

See the demo.

A Decorative Background

Take this darkened circle behind the title in this content block:

Let's make sure it sizes accordingly when the font size changes. But there are more subtle details here. The border-radius should be relative, as well as the thickness of that dashed line.

<section class="block">
  <h3 class="block__title">Content outline</h3>
  <div class="block__content">
    <p>Description to be there....</p>
  </div>
</section>
.block__title {
  position: relative;
  font-size: 1.5em;
  padding: 0.5em;
}

.block__title:after {
  content: "";
  position: absolute;
  left: 0.25em;
  top: 0;
  width: 2.5em;
  height: 2.5em;
  border-radius: 50%;
  background: #000;
  opacity: 0.5;
  transform: scale(1.75);
}

.block__title:before {
  content: "";
  margin-left: 0.5em;
  border-bottom: 0.0625em dashed rgba(255, 255, 255, 0.5);
}

Using relative units for as much as we can here, everything scales:

See the demo.

A Search Input with Icon

It's common to use icons with buttons, but you can use them within inputs too. This is a common example where we indicate a search input by using a magnifying glass icon:

The icon is placed using background-image, and padding-left is added to keep the text from overlapping it. If the font size increases, we should increase the size of that icon commensurately.

<form class="search">
  <label for="search">Enter keyword:</label>
  <input type="search" id="search" placeholder="What are you searching about?">
</form>
.search input {
  width: 25em;
  font-size: 1em;
  padding: 0.625em;
  padding-left: 2.5em;
  border-radius: 0.3125em;
  border: 0.125em solid #b4b4b4;
  background: url("search.svg") left 0.625em center/1.5em 1.5em no-repeat;
}

All the classics are set in relative units here: padding, border, border-radius... but we've also used relative units for both the background-position and the background-size. Now it all scales up nicely!

See the demo.

Toggle

Consider a customized checkbox in the form of a back-and-forth toggle switch:

Nothing in here we can't scale!

<form action="" class="switch">
  <p>Do you want to subscribe?</p>
  <input type="checkbox" name="" id="switch" class="off-screen">
  <label for="switch"><span class="off-screen">Do you want to subscribe?</span></label>
</form>
.switch label {
  width: 5.625em;
  height: 2.5em;
  border: 0.125em solid #b4b4b4;
  border-radius: 2.5em;
}

.switch label:before {
  content: "";
  right: 0.25em;
  top: 0.21875em;
  width: 2em;
  height: 2em;
}

Scale away:

See the demo.

Limiting Line Length, Only When You Need To

Take a block of content like this:

We have quite a bit of horizontal space there. Without limiting anything, the line length of that paragraph would have been a bit too long to be comfortable. Setting max-width is a great way to limit line-length. We probably wouldn't set it in pixels (for all the same reasons we've been covering all along: it doesn't scale) and we probably wouldn't use percentages either, as at narrower widths, 100% is likely fine. Relative units it is!

<div class="hero">
  <h2>This is title for this hero section</h2>
  <p>And this paragraph is a sub title, as you know I'm writing an article about using em units to build dynamic components.</p>
  <a href="#">Read about hero</a>
</div>
.hero h2 {
  margin-bottom: 0.25em;
  font-size: 1.75em;
}

.hero p {
  margin-bottom: 1em;
  max-width: 28.125em; /* limit line length */
  font-size: 1.25em;
}

.hero a {
  display: inline-block;
  background: #4a90e2;
  padding: 0.7em 1.5em;
}

Now the line length is capped, but will happily go full width when less width is available.

See the demo.

SVG Icons in Buttons

One of the reasons people cite liking working with icon fonts is that the font is automatically sized along with the text. But that can also work with <img> (as we've seen), and can work with inline <svg> icons as well!

We'll use em values to set the width and height, and then the icons will scale proportionally as the font scales.

<ul class="social">
    <li class="social__item">
      <a href="#">
        <svg width="32" height="32" viewBox="0 0 32 32">
        <!-- SVG Data -->                    
        </svg> 
        Like on Facebook
      </a>
    </li>
    <li class="social__item">
      <a href="#">
        <svg width="32" height="32" viewBox="0 0 32 32">
        <!-- SVG Data -->                    
        </svg>  
        Follow on Twitter
      </a>
    </li>
    <li class="social__item">
      <a href="#">
        <svg width="32" height="32" viewBox="0 0 32 32">
        <!-- SVG Data -->                    
        </svg> 
        Follow on Dribbble
      </a>
    </li>
</ul>
.social__item svg {
  display: inline-block;
  vertical-align: middle;
  width: 2.1875em;
  height: 2.1875em;
}

See the demo.

List Counters

Say we made a list with custom designed counters. With text set into containers like that, we'd better make sure everything scales proportionately or have a problem like this:

<ul class="list">
  <li>Go to example.com and click on Register</li>
  <li>Enter your email address</li>
  <li>Pick a strong password</li>
  <li>Congrats! You now have an account</li>
</ul>
.list li {
  position: relative;
  padding-left: 3.125em;
  margin-bottom: 1em;
  min-height: 2.5em;
}

.list li:before {
  font-size: 1em;
  width: 2.5em;
  height: 2.5em;
  text-align: center;
  line-height: 2.5em;
  border-radius: 50%;
}

Happy scaling:

See the demo.

Positioned Icon a List/Alert/Modal

I think you're getting the point now, so let's do a few more examples with just a demo where you can see for yourself how relative scaling is helpful:

See the demo.

Hamburger Menu Icon

Perhaps you've "faked" an icon by building it out of elements and pseudo elements. That should be scaleable too.

See the demo.

Gradients

We already looked at how we can use relative units for background-size. We also eluded to the fact that a color stop in a gradient could be set with a relative unit. Let's do just that!

.box-1 {
  background: 
    linear-gradient(
      to right, 
      #4a90e2 0, 
      #4a90e2 0.625em, 
      #1b5dab 0.625em, 
      #1b5dab 1.875em, 
      #4a90e2 0, 
      #4a90e2 3.125em
    );
  background-size: 1.25em 100%;
}

See the demo.

Image Sprites

Some things have fixed sizes that are a lot easier to think about in pixels, like raster images. But that doesn't mean we still can't work with them in relative units. If we combine background-position and background-size in ems, we can use CSS sprites that can scale in size.

See the demo.


Combining em and rem

We've mainly used the em unit throughout this article. We established that the em unit is based on font-size and cascades. But em has a cousin unit: rem. The rem unit is still relative, but relative only to the root (e.g. html {} or :root {}). So it doesn't really cascade like em does, but if you change the root font-size, it will change accordingly.

By combining em and rem, we can keep some sizes fixed and keep the other dynamic. For example, say you wanted the text in these components to be only relative to the root, but have other elements be relative to the more immediate font size. Like the image, for example:

See how adjusting the immediate font-size only affects the image now:

See the demo.


Building a complete site with relative sizes

I built an entire page to demonstrate how we can apply the concept of dynamic components to something real-world and not just isolated to a single example.

Everything here is dynamically sized: logo, tags, title, author, section title, numbered lists, form inputs, buttons, blockquotes... just about everything.

If we were to change the default browser font size from 16px to 20px, this is how the site scales:

Awesome, right? Did you notice how everything scaled, not just the type? That's the beauty of em! 😍

Check out the full page demo.

Note that the font-size changes with a media query breakpoint as well.

Zooming

Our em-based design is happily compatible with browser zooming as well.

While the pixel-based design can run into issues:

Challenges with em

One thing to keep in mind when using em is that when you set the font-size in it, it's based on the nearest parent element with a declared font-size.

.parent {  
  font-size: 20px;
  .child { 
    /* This is based on 20px, so it's 30px */
    font-size: 1.5em;
  }
}

I think we've established that pretty clearly.

But when we size other things in em, it's now based on the newly-adjusted font-size of the current element. For example:

.parent {
  font-size: 20px;
  .child {

    /* This is based on 20px, so it's 30px */
    font-size: 1.5em;
    
    /* This is based on 1.5em (not 20px), so it's also 30px */
    border: 1em solid black;
    
  }
}

It just can be weird to see two different em values in the same element evaluating to the same end value.

This is in addition to the fact that the cascading effect of ems is sometimes challenging in itself. If you size things inside components in ems and those components can be nested, that can cause cascading of sizes that may be undesirable.

Closing up

  • Sizing in pixels is harder to maintain. They aren't relative to anything else. You need to manually adjust all of them to change a proportion. Difficult, time consuming, error prone.
  • Settings values in ems makes things proportional to the font-size, so changing the font-size scales all the values on that element (and cascades to the children).
  • Setting font sizes in pixels may prevent the user from changing their default font size via their browser settings, which is no good for accessibility.

Further reading