In the ever-evolving world of web development, creating flexible, responsive, and accessible designs is crucial. One of the most powerful tools in a developer's CSS toolkit is the use of relative units.
These units allow our designs to be more flexible, responsive, and accessible. In this article, we'll dive deep into the world of relative units, exploring how to use them effectively and why they're so crucial in modern web development.
Why Does It Matter?
Before we delve into the specifics, let's understand why relative units are so vital in modern web development:
- Responsiveness: Relative units are the backbone of responsive design, allowing elements to scale fluidly across different device sizes.
- Accessibility: Using relative units for font sizes ensures that text remains readable when users adjust their browser's default font size.
- Maintainability: With relative units, global layout changes often require updating just a few values, rather than dozens of hardcoded pixel values.
- Consistency: Relative units help maintain proportional relationships between elements, even as the overall scale changes.
Let's explore the various types of relative units and how to use them effectively.
Ems and Rems
The two most commonly used relative units are em
and rem
. Both are based on font size, but they behave differently in important ways.
Understanding Ems
An em
is a unit relative to the font size of the element on which it's used. Here's a detailed example:
.parent {
font-size: 16px;
}
.child {
font-size: 1.5em; /* 24px (16px * 1.5) */
padding: 1em; /* 24px */
border-radius: 0.25em; /* 6px */
}
In this example, all the child element's properties are relative to its own font size. This creates a scalable component where all properties maintain their proportions if the font size changes.
Typically, ems are used for setting font sizes, but as you saw in the example above, their potential extends far beyond that. By using ems, you can define many properties of an element and then easily scale the entire thing up or down with a single change to the font size. This flexibility makes ems incredibly useful for various properties, not just fonts.
A Potential Pitfall
Ems can be tricky when used for defining font sizes of nested elements, as they compound.
Let's look at an example:
<section>
<ul>
<li>First level item</li>
<ul>
<li>Second level item</li>
<ul>
<li>Third level item</li>
<ul>
<li>Fourth level item</li>
</ul>
</ul>
</ul>
</ul>
</section>
Now, let's apply some CSS using em
:
section {
font-size: 16px;
}
ul {
font-size: 0.9em;
}
This will result in this:
Let's break down what happens at each level:
- First level - Font size: 0.9em of 16px = 14.4px
- Second level - Font size: 0.9em of 14.4px = 12.96px
- Third level - Font size: 0.9em of 12.96px = 11.66px
- Fourth level - Font size: 0.9em of 11.66px = 10.50px
As you can see, the font size gets progressively smaller with each nested level. This compounding effect can lead to text becoming unreadably small in deeply nested structures.
This is both a feature and a potential pitfall, depending on your design needs.
Rems: Root Ems
To avoid the compounding issue with em
, we have rem
units. These are always relative to the root element's font size (usually the <html>
element).
Let’s suppose for the next examples, the current font-size
of the root element is 16px
:
.deeply-nested-element {
font-size: 1.5rem; /* Always 24px, regardless of nesting */
padding: 1rem; /* Always 16px */
margin: 0.5rem; /* Always 8px */
}
Rems provide consistency across your document, making them ideal for font sizes and many other properties!
Practical Example
Let's create an accessible button component that scales nicely using a combination of em
and rem
:
.button {
font-size: 1rem; /* 16px (or the prefered size set by the user) */
padding: .5em 1em; /* 8px 16px, using em to scale with the current element font-size */
border-radius: .25em; /* 4px */
transition: font-size .3s ease, padding .3s ease, border-radius .3s ease;
}
@media (prefers-reduced-motion: no-preference) {
.button:hover {
font-size: 1.1rem; /* Grows to 17.6px on hover */
}
}
In this example, the button's padding and border-radius will scale proportionally if the user changes the font size of the html document :)
Viewport-Relative Units
While ems and rems are incredibly useful, sometimes we need units that are relative to the size of the viewport (screen). That's where viewport-relative units come in:
-
vw
: 1% of the viewport width -
vh
: 1% of the viewport height -
vmin
: 1% of the smaller dimension (width or height) -
vmax
: 1% of the larger dimension (width or height)
These units are particularly useful for creating full-screen layouts or elements that need to scale dramatically with the viewport size.
.hero-section {
height: 100vh;
width: 100vw;
/* ... */
}
.hero-section-image {
width: 50vmin;
height: 50vmin;
object-fit: cover;
/* ... */
}
This creates a full-height hero section with content that scales based on the viewport size, and an image that's always square and takes up half of the smaller viewport dimension.
While these units are great, they were originally designed with desktop browsers in mind, where the viewport size remains constant unless the user manually resizes the window.
On mobile, it’s an other story.
The Dynamic Nature of Mobile Viewports
Many mobile browsers implement a user experience feature where the browser's UI elements, such as the address bar and navigation buttons, can appear or disappear based on user interaction. Here's how it typically works:
- When a user first loads a page, these UI elements are visible, reducing the available viewport height.
- As the user scrolls down the page, these elements often slide out of view, effectively increasing the viewport height.
- When the user scrolls back up or taps near the top of the screen, these elements reappear, once again reducing the viewport height.
This dynamic behavior creates a fluctuating viewport size, which can lead to layout issues when using viewport-relative units like vh
(for overlays, side-bars, etc…).
Enter Dynamic Viewport Units
To address these issues, new viewport units were introduced:
-
svh
(Small Viewport Height): Represents the viewport height when mobile browser UI elements are visible. -
lvh
(Large Viewport Height): Represents the viewport height when mobile browser UI elements are hidden. -
dvh
(Dynamic Viewport Height): Dynamically adjusts betweensvh
andlvh
as the browser UI elements appear or disappear.
We have the same properties for width, and for smaller and larger dimension (width or height):
s(vh | vw | vmin | vmax)
l(vh | vw | vmin | vmax)
d(vh | vw | vmin | vmax)
Consider this example:
.side-menu {
height: 100dvh;
}
This element will dynamically adjust its height based on the current state of the mobile browser's UI elements, providing a more consistent user experience 👍.
Do not use
dvh
for scrollable sections: Imagine scrolling past multiple full-screen sections sing100dvh
, then scrolling up slightly. The content suddenly jumps waaaaay up.
This can cause major lag and it forces the browser to recalculate the entire page layout, which can hurt performance (layout thrashing).
Inline / Block Logical Properties
To ensure completeness, it’s important to mention an additional set of unit types: inline and block. These are known as "logical properties”. They function similarly to width and height but are designed to adapt to vertically-written languages like Mongolian, effectively transposing their behavior. These are vi
, vb
, svi
, svb
, lvi
, lvb
, dvi
, dvb
.
Browser Support and Fallbacks
Although support for dynamic viewport units is 93.21% (at the time of writing), you might want to provide fallbacks if you target older browsers:
.modal {
height: 100vh; /* Fallback for browsers that don't support dvh */
height: 100dvh; /* Will be used by browsers that support it */
}
Combining Units
Using calc
Function
The calc()
function is a game-changer for responsive design. It allows you to perform basic math operations with different units, making it easier to create flexible layouts. Here's what you need to know:
- Supports addition (
+
), subtraction (-
), multiplication (*
), and division (/
) - Always add spaces around operators
Example:
p {
font-size: calc(0.5em + 1svw);
}
In this example, the font size will scale with the viewport width but will never get smaller than 0.5rem
.
Note: Always include em or rem units when using viewport units for font sizes. This ensures user font preferences are respected.
Using clamp
Function
The clamp()
function takes this concept even further, allowing you to set a preferred value with a minimum and maximum. clamp()
takes three arguments:
- Minimum value
- Preferred value (can be an expression)
- Maximum value
Have a look at this example:
h1 {
font-size: clamp(1em, 5vw, 6em);
}
This sets a font size that scales with the viewport width but is never smaller than 1em
or larger than 6em
.
Using Other Useful Functions
-
min()
: Chooses the smallest value from a list- Example:
width: min(200px, 20svw);
- Example:
-
max()
: Chooses the largest value from a list- Example:
min-height: max(200px, 20svw);
- Example:
Wrapping up
Mastering relative units in CSS is a game-changer for creating flexible and accessible designs. By understanding the nuances of ems, rems, viewport units, and how to combine them effectively, you can create layouts that adapt seamlessly to different screen sizes and user preferences.
The introduction of dynamic viewport units like dvh
represents an important step in adapting web design to the unique challenges of mobile browsers. By using these units, you can create responsive layouts that handle the dynamic nature of mobile viewports.
However, the goal is not to eliminate pixels entirely, but to use them judiciously. Absolute units like pixels still have their place, especially for borders or when you need pixel-perfect control. The key is knowing when to use relative units for flexibility and when to use absolute units for precision.
As you build your next project or your next UIs, challenge yourself to use relative units wherever possible. You'll likely find that your stylesheets become more concise, your layouts more flexible, and your overall design more robust across different devices and user settings.
Until next time, happy coding!