Breaking Out With Viewport Units and Calc
At 4/19/2024
While iterating on a new article layout for the impending Cloud Four redesign, I encountered an old CSS layout problem.
For long-form content, it’s usually a good idea to limit line lengths for readability. The most straightforward way to do that is to wrap the post content in a containing element:
.u-containProse {
max-width: 40em;
margin-left: auto;
margin-right: auto;
}
Code language: CSS (css)
<div class="u-containProse">
<p>...</p>
<p>...</p>
</div>
Code language: HTML, XML (xml)
But what if we want some content to extend beyond the boundaries of our container? Certain images might have greater impact if they fill the viewport:
In the past, I’ve solved this problem by wrapping everything but full-width imagery:
<div class="u-containProse">
<p>...</p>
</div>
<img src="..." alt="...">
<div class="u-containProse">
<p>...</p>
</div>
Code language: HTML, XML (xml)
But adding those containers to every post gets tedious very quickly. It can also be difficult to enforce within a content management system.
I’ve also tried capping the width of specific descendent elements (paragraphs, lists, etc.):
.u-containProse p,
.u-containProse ul,
.u-containProse ol,
.u-containProse blockquote/*, etc. */ {
max-width: 40em;
margin-left: auto;
margin-right: auto;
}
Code language: CSS (css)
Aside from that selector giving me nightmares, this technique might also cause width
, margin
or even float
overrides to behave unexpectedly within article content. Plus, it won’t solve the problem at all if your content management system likes to wrap lone images in paragraphs.
The problem with both solutions is that they complicate the most common elements (paragraphs and other flow content) instead of the outliers (full-width imagery). I wondered if we could change that.
Flipping the Script
To release our child element from its container, we need to know how much space there is between the container edge and the viewport edge… half the viewport width, minus half the container width. We can determine this value using the calc()
function, viewport units and good ol’ percentages (for the container width):
.u-release {
margin-left: calc(-50vw + 50%);
margin-right: calc(-50vw + 50%);
}
Code language: CSS (css)
Voilà! Any element with this class applied will meet the viewport edge, regardless of container size. Here it is in action:
See the Pen Full-width element in fixed-width container example by Tyler Sticka (@tylersticka) on CodePen.
Browsers like Opera Mini that don’t support calc()
or viewport units will simply ignore them.
One Big, Dumb Caveat
When I found this solution, I was thrilled. It seemed so clever, straightforward, predictable and concise compared to my previous attempts. It was in the throes of patting myself on the back that I first saw it…
An unexpected scrollbar:
On any page with this utility class in use, a visible vertical scrollbar would always be accompanied by an obnoxious horizontal scrollbar. Shorter pages didn’t suffer from this problem, and browsers without visible scrollbars (iOS Safari, Android Chrome) seemed immune as well. Why??
I found my answer buried deep in the spec (emphasis mine):
The viewport-percentage lengths are relative to the size of the initial containing block. When the height or width of the initial containing block is changed, they are scaled accordingly. However, when the value of overflow on the root element is auto, any scroll bars are assumed not to exist. Note that the initial containing block’s size is affected by the presence of scrollbars on the viewport.
Translation: Viewport units don’t take scrollbar dimensions into account unless you explicitly set overflow
values to scroll
. But even that doesn’t work in Chrome or Safari (open bugs here and here).
I reacted to this information with characteristic poise:
Luckily, a “fix” was relatively straightforward:
html,
body {
overflow-x: hidden;
}
Code language: CSS (css)
It’s just a shame that it’s even necessary.