Skip to main content

Breaking Out With Viewport Units and Calc

By Tyler Sticka

Published on May 26th, 2016

Topics

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:

Mockup of container with full-width element

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.

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.

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:

Not reacting with poise at all

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.

Comments

Catherine Azzarello said:

Thanks for the post, Tyler! I’ve had the same problems with sites. My solution has been to hand-code the HTML and drop it into CMS…rows w/o margins or padding on left and right for bleed images, and all the rest of content within rows & columns to keep line lengths contained.

Not surprised about the scrollbar. They’re troublesome. Don’t even get me started on old versions of Opera (as used by many set-top-boxes) and writing hideous code to hide those babies!

Mike Street said:

Great article Tyler. Always good to see how other people achieve similar results. I will definitely bear this in mind next time I need to do the same thing.

We have done it on our website using :before and :after elements – positioned absolute (this can be seen in action on the code and image blocks on this blog post – https://www.liquidlight.co.uk/blog/article/creating-a-custom-mailchimp-template-with-layout-variations/)

Mettin Parzinski said:

Didn’t you experience any blurry-ness? I’ve used a similar technique but text would snap to half pixels because of the percentages used.

Emil said:

As a side note, using negative margins to expand elements is a really useful technique. One tiny caveat (which doesn’t affect the usefulness of this technique, but is good to know regardless) is that negative margins only work in this expanding way on non-floating elements with no explicit width declared. If the released element had a width-declaration (http://codepen.io/thatemil/pen/mEyPmb) or was floated (http://codepen.io/thatemil/pen/KMwzvq) the margins work differently.

(Specifically, a margin opposite to the text-start direction or opposite of the float direction will pull in adjacent elements, rather than pull out the element they’re being applied to)

avr said:

One issue that I just ran into last week with html, body { overflow-x: hidden; } is that jQuery’s .on('scroll') events weren’t firing.

Stack Overflow helped: http://stackoverflow.com/questions/5686629/jquery-window-scroll-event-does-not-fire-up

Does your technique cause any issues with scroll?

Julián Landerreche said:

I’ve been tinkering with this trick lately. It’s a nice way to solve the issue of “breaking out” the container box for certain elements.

That said, the selectors used for your previous way of solving it (by capping the width of inner elements) can be simplified to this:

.u-containProse > * {
  max-width: 40em;
  margin-left: auto;
  margin-right: auto;
}

.u-containProse .u-release {
  max-width: none;
}
Code language: CSS (css)

And you could write it even shorter:

.u-containProse > *:not(.u-release) {
  max-width: 40em;
  margin-left: auto;
  margin-right: auto;
}
Code language: CSS (css)

Adam Norwood said:

Good technique!

For a site I built last year, I solved this problem using a margin-left: -50vw combined with a left: 50%, like so: http://codepen.io/adamnorwood/pen/wWzKBQ I can’t remember where I learned about this method, but it’s held up pretty well and I don’t remember what the downside is (anyone? I guess trying to absolute position things inside the released container could be kind of weird, but hopefully that’d be an edge case).

Comparing it to the technique described here, and looking at the caniuse charts, the only advantages seem to be that it doesn’t rely on calc() (so slightly better support in Android browsers), and it doesn’t require the overflow-x: hidden; tweak, which might be helpful if overriding the native scroll turns out to be problematic for some reason.

Anyhow, just wanted to add another possible solution to consider!

Replies to Adam Norwood

Tyler Sticka (Article Author ) replied:

Nice spin on this technique!

Unfortunately, your pen suffers from the same horizontal scrollbar issue I encountered (screenshot). Curses!

Replies to Tyler Sticka
Adam Norwood replied:

Oh, crazy! I wasn’t seeing the horizontal scroll bar because I have OS X’s “Show scroll bars” preference (under System Preferences -> General) set to “When scrolling” instead of “Always”. Set to “when scrolling”, the vertical scroll appears and disappears as expected and there’s no trace at all of the unwanted horizontal bar. Setting it to “always” I now see the problem with your code as well as mine. This setting affects all three major browsers on OS X. So overflow-x: hidden it is!

Thanks for the follow-up, it’s a good reminder to check for that setting when testing out sites…

Tyler Sticka (Article Author ) replied:

I found a drawback with this alternative. Although this sidesteps the calc dependency (which is great), browsers that don’t support viewport units will still apply left: 50%. Using calc (which has greater browser support than viewport units anyway) insures no layout change occurs unless the browser supports 100% of the technique.

Gabriel Luethje said:

It would be nice to have a bit more info about the solution at the end (adding overflow-x:hidden to body and html elements). Maybe it’s just me but that makes me a bit uneasy, and feels like there could be unintended consequences.

I did some searching and all I could come up with was potential affects on jQuery’s scrollTop() method.