The hacktastic zoom fix

Everyone keeps asking me what is with all the “x” in the OOCSS grids… so I finally wrote an article. The short answer is that it isn’t just a clearfix (it does that too), but it also causes the element to stretch wide, even when it has very little content. It is a bit of magic that allows us to use display table-cell to create a new formatting context in all browsers. It also allows us to solve sub-pixel rounding errors without resorting to fixed widths. (This has all kinds of great perf benefits because the grids are nestable and stackable).

  content:" x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x ";
}

I wrote a while back about creating new formatting contexts with overflow: hidden. A new formatting context is desirable because it tells an element not to wrap around nearby floats, and to clear any floats it contains. Essentially a new formatting context makes an element behave like a column. Anyone who has tried to create a grids system or multi-column layout knows that that is no easy feat.

Zoom Baby

This is one case where Internet Explorer gets it right. Zoom triggers hasLayout, which creates a new formatting context, clears floats, and prevents float wrapping with none of the drawbacks of the standard solutions.

.lastUnit{
  zoom:1;
}

I wish we had a simple property value pair that would do the same thing in standards-based browsers.

.lastUnit {
  formatting-context: new; /* please! */
}

There are several ways of achieving the same thing in Safari, Firefox, Chrome, and Opera, but they each have drawbacks. For example:

Overflow Hidden Hides Stuff

Overflow hidden creates a new formatting context with very few side effects, and for many websites, it will still be the right answer, however it does have one major drawback. If any content within the element overflows its boundaries, it will be cropped. Normally, if you don’t specify a height (you shouldn’t need to), the element grows to the size of it’s content.

 .lastUnit {
  overflow: hidden;
}

However, on a complex site like facebook (and most applications), popovers, fancy buttons, and menus were cropped off because they were outside of the normal document flow and didn’t influence the height of the parent node. This forced me to look for another solution.

Table Cells Create a New Formatting Context

In the specification, several property value pairs create a new formatting context including; overflow (other than visible), table-cell, inline-block, and float. I set out to try them all. Other variations of overflow triggered a scroll bar that didn’t make me happy, and one of them (sorry, it was a year ago, I can’t remember) caused issues in RTL.

I began to test display: table-cell. I used the OOCSS grids test page in which each grid contains a heading and a paragraph filled with lorem ipsum text. To my astonishment, display table cell worked.

.lastUnit {
  display: table-cell;
}

The HTML of the last unit.

<div class="unit size1of5 lastUnit">
<h3>1/5</h3>
<p>i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i i </p>
</div>

I was very pleased with myself, until I looked at the modules page. All of the modules in the last column were shrink-wrapped to the text. I began to explore what was different about my working grids and my broken modules. You can see the same issue on the facebook comments page. Notice how the comment box is far too narrow.

Comment box should be full width, but display table cell is shrink wrapping content.

I discovered that display table cell shrinks or grows to fit the content (much like a floated element). On my grids test page, the lorem ipsum dummy text was actually pushing the last column open to take all the remaining space on the line. I was getting the behavior I wanted, but it was triggered by the content, which is unacceptable for OOCSS, because OOCSS requires a strict separation of container and content. I couldn’t require text in the grids, in fact, I have no idea how people will use them.

Generated Content FTW

I realized that generated content was the only way to insert this text without requiring developers to muck up their HTML. As a bonus, generated content is not a part of the DOM, so it should be ignored by screen readers. I took a look at the specification and used the YUI clearfix implementation as a starting point for my solution (This is where things get a little bit crazy). My column, lastUnit, is still set to display table-cell, but we also add the following code:

.lastUnit {
  display: table-cell;
}
.lastUnit:after {
  clear: both;
  display: block;
  visibility: hidden;
  overflow: hidden;
  height: 0 !important;
  line-height: 0;
  font-size: xx-large;
  content: " . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ";
}

Crazy, right? So let me show you what I’m doing here.

Understanding the Code

First, open the module test page in Firefox with Firebug enabled. The modules are inside thirds grids, which use the technique. Change visibility so that you can see how it works.

visibility: visible;

The generated text pushes my lastUnit open in much the same way the lorem ipsum dummy text did in the grids test page. They convince the browser that this element needs all of the space left after the width of floated siblings has been calculated.

Lets go through it piece by piece:

.lastUnit {
  zoom: 1;
  display: table-cell;
}

First, we create a new formatting context so that floats are not wrapped, margins do not collapse, etc. Display table cell is not well supported in older versions of IE, but it doesn’t matter because zoom does the trick. None of the rest of the code applies to IE.

.lastUnit:after {

We’re using the :after pseudo class so we can insert generated content (text) as the last node inside the lastUnit.

  clear: both;

Makes the generated content clear any non-generated sibling elements.

  display: block; 

We want the element to expand to the full width.

  visibility: hidden; 

Makes the generated content invisible and allows clicks and interaction with any content that might be below.

  overflow: hidden;

Makes sure height is respected by the browser, even in browsers that expand containers (an IE bug).

  height:0 !important; 

We don’t want generated content changing the layout.

  line-height: 0; 

Again, generated content shouldn’t change the layout. We’re covering all our bases.

  font-size: xx-large; 

Large text means we’ll need less generated content.

  content:" x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x ";
}

The faux text does the heavy lifting by opening to full width the element which is in display table-cell.

Finally, An Opera Bug

Someone on the google group noticed that Opera was choking on this method. It seems that Opera cannot wrap a series of “. . .” as if it were normal text (a bug report was submitted, so this may have been corrected already). Simply changing all of the dots to “x” resolved the issue. Any text will work, Easter egg anyone?
We changed that line to:

content:" x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x ";

Drawbacks

Table-cells interact with each other in weird ways. Unlike other elements, which only interact with siblings and parent nodes when they change dimensions, table cells are weird and hard to calculate because the browser does a lot of fudging to make them line up and layout nicely. This fudging has proven to be expensive, so I do wonder about the performance impacts of this approach. Perhaps it is possible to set the display to fixed as Jenny Han Donnelly of YUI suggested — something to test another day.
Generally I avoid table cell whenever possible, but in this case I had tried all of the other methods for generating a new formatting context. Ultimately, browser vendors should allow a trigger that doesn’t have side effects.

Avoiding Pseudo-Bugs

It might be tempting to try to position our last column without creating a new formatting context, for instance, modern browsers are much better at calculating percentage widths. OTOH, zoom creates a new formatting context for IE and it is important to keep the flow as similar as possible across browsers to avoid unintended consequences.

It’s all about working with CSS rather than against it. That means that if you have a new formatting context in one browser you should have it in all of them.

47 thoughts on “The hacktastic zoom fix”

    1. @Nicholas – It is really frustrating how screen readers don’t seem to adhere to the specification. It makes it very challenging to do the right thing, even as a developer who cares.

      . caused display issues in Opera. If they have fixed it, we could consider that.

      0a0 is untested. What character is that? Ideally we want a wide character so it takes as much space as possible.

  1. I haven’t tested this, but wouldn’t setting font-size to something rediculous like 2000em allow you to use even less generated characters?

    (and as an aside: thanks for the explanation! I’ve used your oocss as a starting point, and was wondering about some of the rules…)

  2. @Nicole: I would have thought that a NBSP character would be a safer bet that other forms of punctuation.

    @Jaron: Unfortunately, setting a massive font-size breaks Opera 10 again (although it works in Firefox and Chrome).

  3. Interesting stuff. To add to Nicholas’ suggestion perhaps something innocuous like an em-dash could work? It’s a wide character too. Or any other similar unicode character like an em-space might be better eg. { content: ” \2003 \2003 \2003 etc…” }

  4. Some remarks. :)

    - Did you try display:table instead of display:table-cell? It may have other issues (like top and bottom margins not collapsing with sibling elements in Firefox), but when you have two siblings with this fix they won’t try to act as a two-column table.

    - A word of warning with generated content: it makes layouts hard to debug when something goes wrong, since you can’t inspect generated content in Firebug or Web Inspector. I encountered a bug with a WordPress template “framework” where there were inexplicable blank lines… caused by clearfix-styled generated content. With the right styles you avoid this issue, but the important part here is that it took a lot of time to figure out since the generated content couldn’t be inspected.

    - Now if we’re going to use generated content, and the only goal is to avoid floated children and descendants to overflow, the classic “clearfix” solution looks like much less hassle! No big string of characters, and more importantly the ability to choose whatever type of display type you want (display:block as a default, and only use table, table-cell or inline-block when you mean to).

    - One drawback of this technique, or the classic clearfix solution with some zoom:1 thrown in, is that you end up triggering hasLayout in IE6-7. Not in IE8, of course, since there is no hasLayout in IE8 (hallelujah!). Since generated content only works in IE8, you need the hasLayout even if you only support IE7 and higher. Meanwhile, creating a new formatting context with overflow:hidden works in IE7, so if you’re not supporting IE6 you have the option to only use overflow:hidden, not triggering hasLayout.

    - Ideally we would need some CSS properties such as formatting-context: none|new|inherit; float-overflow: allow|restrict|inherit; and margin-collapse: collapse|separate|inherit;. The first one would set the other two ones to “allow”+”collapse”, or “restrict”+”separate”. Who writes the CSSWG about that?

  5. This hack breaks in Opera when it used next to floats, and IMHO Opera is right to do this.

    Table layout algorithm:

    http://dev.w3.org/csswg/css3-tables-algorithms/Overview.src.htm

    allows cell to expand to “Cell Intrinsic Preferred Width”, and refers to width described in 10.3.5 and 10.3.7 in CSS 2.1.

    Referenced shrink algorithm in CSS 2.1:

    http://www.w3.org/TR/CSS2/visudet.html#float-width

    incudes:

    max(preferred minimum width, available width)

    and “available width” is described as width of containing block. So table-cell is allowed to take full width to push floats or drop below them.

  6. Thanks for the explanation, I normally use “table-cell” and clearing floats, but not using the content fix.
    Good work ;)

  7. Great explanation, I’ve wondered about this for a while too. Very interesting to see the process involved in getting to that solution.

  8. Why not just use display:inline-block? It’s not supported only by Firefox 2.0 which is negligible. But it can be relative positioned in Firefox unlike table elements that’s useful.

  9. If you can put a width on the column, I suggest this:

    display: inline-block;
    width: ???;

    Cordially,
    David

  10. @Nicole: pardon my ignorance, but what is the exact purpose of the lastUnit class? I can’t find any mention of that in the documentation, other than that it “should be applied to the last child of every line”.

  11. @Nicole: now I get it :) So it’s more or less a treatment for older versions of IE, or are there other browsers that don’t get the maths right?

  12. I don’t fix for Opera, ever – it’s too small of a market share and unless your doing a web-specific site (something like css-tricks.com) the percentage of visitors that use Opera in on a standard site is so small it’s not worth my time.

    1. Opera’s market share is variable by country. Since I don’t know where the framework will be used, I’m happy to make an accommodation like changing from a dot to an “x”. Particularly because it has no negative impact on other browsers. Plus, Opera users are really vocal. ;)

  13. @Anders: Here is simple example of sub-pixel rounding: You have 954px main layout and inside you have 5 column grid or 5 x 20%. So one unit(column) will be 954px / 5 = 109,08px.You can’t split the pixel. Browsers will round this to 191px or you will have 191px x 5 = 955px. So from 954px your layout it will become 955px and that pixel will destroy your layout in IE6/7. Other browsers have margin of tolerance and don’t crash.

  14. AFAIK screen-readers ignore content hidden by visibility: hidden (or display: none) so the issue from comment #1 isn’t affecting this scenario, i guess…

  15. thanks Nicole. always fun to learn new stuff. I knew a few of those things but it’s always great to learn that there’s more to what you thought you knew.

  16. What browser is that screenshot from? I’d be much obliged if you could show me how to repro so we can fix it :). Also, this reminds me of the infamous image-block implementation that we worked on at Facebook awhile ago. We ended up adding a width:10000px to the element that has the display:table-cell, which I think achieves the same thing you’re going for. Surprisingly, the enormous width does not cause the element to stretch beyond the width of its container.

  17. @Nan – I’ve been playing around with that same method, but found that IE6 + 7 just stretched to be 10000px. How did you guys get around that?

    Thanks.

  18. @Matt – IE6 + 7 don’t use display:table-cell in OOCSS grids. You’ll find *display:block and *zoom:1 in the grids.css. So if you are experimenting with width:10000px, you have to add *width:auto for our old friends.

  19. Has anyone tested the width:10000px version on IE9 yet? It works perfectly on FF2+, Chrome1+, Safari3+, Opera9+ and IE6-8. I think this is the way to go. I’m using it in my next project. :)

  20. Heya Nicole,
    Thanks for posting this up :)

    Have you tested this out in Webkit when the lastUnit has an element (for instance, an image) that is greater width than the allowable space?
    In every other engine, the grids are maintained, but in webkit, the column drops down to a new row.
    I tested this out on Facebook and this happens there as well in Webkit.

    Have you seen this before? Any ideas on working around it?

    Thanks :)

    1. @Nate – great question. I’m thinking of making a wrapper that adds a scroll when necessary to the image. Ugly, but effective. The low tech solution is simply to control what you put in and make sure it isn’t too big. ;)

Comments are closed.