Breaking Out With Viewport Units and Calc

Written by Tyler Sticka on

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;
}
<div class="u-containProse">
  <p>...</p>
  <p>...</p>
</div>

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>

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;
}

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%);
}

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:

Not reacting with poise at all

Luckily, a “fix” was relatively straightforward:

html,
body {
  overflow-x: hidden;
}

It’s just a shame that it’s even necessary.

Tyler Sticka

Tyler Sticka is Cloud Four’s Lead Designer, allowing him to think about responsive and component-based design almost every day. When he isn’t sketching on sticky notes, experimenting with SVG or nitpicking design details with his coworkers, he enjoys reading comics, making video games and listening to weird music. He tweets as @tylersticka.

Comments

Add a comment

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!

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/)

Replies

Thanks for sharing these techniques! I've always thought having extra divs for full-bleed stuff was hack-y too...the one caveat with Ryan Canfield's technique is that the pseudo elements get painted based on the initial viewport width. If you re-size the browser window to be larger, those elements don't expand unless the page is refreshed. If you resize it smaller, no problem. It's a bit nit-picky but if you want that element to be full bleed in all instances your technique is probably a better choice.

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.

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)

Maybe:
.u-release {
margin-left: calc(-50vw + 50% - 0.5em);
margin-right: calc(-50vw + 50% + 0.5em);
}

?

Not sure why, but I didn't understand the math until I rewrote it as calc(50%-50vw).

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;
}

And you could write it even shorter:

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

Actually, this works better

margin: 0 calc(-50vw + 50%);
padding: 0 calc(50vw - 50%);

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

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…

You're entirely correct, and I should have noticed that fallback flaw. Thanks for considering the alternative technique, but more importantly for pointing out that issue! Looks like calc() is the winner.

Thank you for this helpful tip! That was my first heads up about vw. Your "That's it, I quit" gif cracked me up XD

Leave a Comment

Please be kind, courteous and constructive. You may use simple HTML or Markdown in your comments. All fields are required.


Let’s discuss your project! Email Us