10

My web GUI's layout is partially driven by CSS tables. This is mainly because I want the "cells" to have the same height under all situations without any massive headaches surrounding alignment. Overall, this approach has been very successful.

However, I do have a problem whereby the right-hand cell in the table can sometimes take a moment to render, causing the left-hand cell to briefly have 100% of the page width. This causes a noticeable "flicker" effect which, although minor, is kind of annoying. And I've decided to fix it.

Here is a vague representation of how my page works:

#tbl      { display: table; width: 200px; border: 1px solid black; }
#tbl-row  { display: table-row; }
#tbl-col1,
#tbl-col2 { display: table-cell; }

#tbl-col1 { width: 50px; background-color: red; }
#tbl-col2 { background-color: blue; }
<div id="tbl">
    <div id="tbl-row">
        <div id="tbl-col1">LHS</div>
        <div id="tbl-col2">RHS</div>
    </div>
</div>

All's well and good until you use your developer tools to give #tbl-col2 a display: none directive, [I hope accurately] simulating the state of the browser's rendering engine in the moments between #tbl-col1 having been rendered, and #tbl-col2 being rendered.

Notice that #tbl-col1 immediately takes up 100% of the width of the table, despite the width I've given it. I sort of understand why this is happening: after all, I've asked the browser to make the divs behave like tables. Still, it's undesirable here.

I tried to fix this by inserting a "spacer", hoping that without a width it would expand to fill all the space on the right-hand side until such time as the right-hand side got rendered:

#tbl      { display: table; width: 200px; border: 1px solid black; }
#tbl-row  { display: table-row; }
#tbl-col1,
#tbl-spc,
#tbl-col2 { display: table-cell; }

#tbl-col1 { width: 50px;  background-color: red; }
#tbl-col2 { width: 150px; background-color: blue; }
<div id="tbl">
    <div id="tbl-row">
        <div id="tbl-col1">LHS</div>
        <div id="tbl-spc"></div>
        <div id="tbl-col2">RHS</div>
    </div>
</div>

As you can see by again hiding #tbl-col2, it made not a blind bit of difference: #tbl-col1 still took the whole width of the table, rather than the 50px I allowed it.

Assuming I'd rather fix this than abandon the CSS Tables layout altogether, what can I do?

Or am I going to have to replace the layout or, even worse, take a JavaScript approach to resolving the FoUC?

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • It a) may not help and b) not fit your design paradigm, but can you give #col2 a `min-width`? – Jon P Jun 25 '15 at 22:53
  • @JonP: Doesn't seem to help :( – Lightness Races in Orbit Jun 26 '15 at 09:48
  • This is the nature of the table columns to cover the whole width inside the table. So any width given that stops the table column to cover it's table full width will not be accepted You can not break the global rule. If you have two column than onc can retain the width given but other will fulfill the further gap. You should play with table width instead table columns or use display inline-block for you structure wisely. – Raj Jun 27 '15 at 20:34
  • If you could switch away from `display: table;` it would be quite easy to create columns, [maybe a solution similar to this](http://getbootstrap.com/css/#grid) – maioman Jun 27 '15 at 20:45

4 Answers4

3

What I like to do in similar cases (for example html emails) is to pre-define column widths using empty cells this way:

.tbl {
    display: table;
    width: 200px;
    border: 1px solid black;
    color: #fff;
}
.tbl-row {
    display: table-row;
}
.tbl-cell {
    display: table-cell;
}
.tbl-col1 {
    width: 50px;
    background-color: red;
}
.tbl-col2 {
    background-color: blue;
}
<h3>Pre-define columns width by adding additional row</h3>

<div class="tbl">
    <div class="tbl-row tbl-format">
        <div class="tbl-cell tbl-col1"></div>
        <div class="tbl-cell tbl-col2"></div>
    </div>
    <div class="tbl-row">
        <div class="tbl-cell tbl-col1">LHS</div>
        <div class="tbl-cell tbl-col2">RHS</div>
    </div>
</div>

That formatting column is invisible if there is no content inside cells, but still it tells browser the way table should be formatted.

skobaljic
  • 9,379
  • 1
  • 25
  • 51
  • This is good. Alas for reasons outside of the scope of this question (roughly speaking, some pages may have different column widths/numbers, and the way the site templates are arranged makes it difficult to "mirror" these changes back up to the formatting row defined in the header template) I'm not sure it's the most effective for me. Definitely worth bearing in mind, though. – Lightness Races in Orbit Jul 01 '15 at 10:31
  • Still, actually, I think this is my favourite approach. Oriol's is "cleverer" but this avoids JavaScript, delayed visibility, ... Might be worth fixing the issue I described in my previous comment in order to get this solution. – Lightness Races in Orbit Jul 01 '15 at 10:44
  • Meh I wanted to add a 250 bounty here too as a prize, but 500 is the only bounty I'm allowed to add now apparently – Lightness Races in Orbit Jul 01 '15 at 16:02
2

That's because, by default, tables use the automatic table layout.

The CSS 2.1 spec doesn't define that layout mode, but suggests a (non-normative) algorithm, which reflects the behavior of several popular HTML user agents.

According to that algorithm,

If the used width is greater than MIN, the extra width should be distributed over the columns.

However, it doesn't explain how it should be distributed. In fact, your attempt of inserting a "spacer" element works perfectly on Firefox, but not on Chrome.

Instead, you may want to try the fixed table mode, which is properly defined in the spec (and thus more reliable), is usually faster, and solves the problem too:

In the fixed table layout algorithm, the width of each column is determined as follows:

  1. A column element with a value other than auto for the width property sets the width for that column.
  2. Otherwise, a cell in the first row with a value other than auto for the width property determines the width for that column. If the cell spans more than one column, the width is divided over the columns.
  3. Any remaining columns equally divide the remaining horizontal table space (minus borders or cell spacing).

According to the 3rd point, the spacer element will receive the remaining 150px before the last cell has been loaded. And will receive the remaining 0px once loaded.

So you need

#tbl { table-layout: fixed; }

.tbl {
  display: table;
  table-layout: fixed;
  width: 200px;
  border: 1px solid black;
}
.tbl-row {
  display: table-row;
}
.tbl-col1,
.tbl-spc,
.tbl-col2 {
  display: table-cell;
}
.tbl-col1 {
  width: 50px;
  background-color: red;
}
.tbl-col2 {
  width: 150px;
  background-color: blue;
}
.hide {
  display: none;
}
While parsing the first cell:
<div class="tbl">
  <div class="tbl-row">
    <div class="tbl-col1">LHS</div>
    <div class="tbl-spc hide"></div>
    <div class="tbl-col2 hide">RHS</div>
  </div>
</div>

While parsing the spacer:
<div class="tbl">
  <div class="tbl-row">
    <div class="tbl-col1">LHS</div>
    <div class="tbl-spc"></div>
    <div class="tbl-col2 hide">RHS</div>
  </div>
</div>

While parsing the second cell:
<div class="tbl">
  <div class="tbl-row">
    <div class="tbl-col1">LHS</div>
    <div class="tbl-spc"></div>
    <div class="tbl-col2">RHS</div>
  </div>
</div>

However, there is still a problem: the spacer won't be displayed until the first cell has been parsed completely.

That means the spacer must be loaded first, but must not be the first to be displayed. Sadly, the CSS table layout does not allow to reorder the cells. But it can be achieved by removing the non-semantic spacer from the HTML and using an ::after pseudo-element instead:

#tbl-row:after {
  content: '';
  display: table-cell;
}

.tbl {
  display: table;
  table-layout: fixed;
  width: 200px;
  border: 1px solid black;
}
.tbl-row {
  display: table-row;
}
.tbl-row:after {
  content: '';
  display: table-cell;
}
.tbl-col1,
.tbl-col2 {
  display: table-cell;
}
.tbl-col1 {
  width: 50px;
  background-color: red;
}
.tbl-col2 {
  width: 150px;
  background-color: blue;
}
.hide {
  display: none;
}
While parsing the first cell:
<div class="tbl">
  <div class="tbl-row">
    <div class="tbl-col1">LHS</div>
    <div class="tbl-col2 hide">RHS</div>
  </div>
</div>

While parsing the second cell:
<div class="tbl">
  <div class="tbl-row">
    <div class="tbl-col1">LHS</div>
    <div class="tbl-col2">RHS</div>
  </div>
</div>
Oriol
  • 274,082
  • 63
  • 437
  • 513
  • Mm, seems I do need to add JavaScript to remove that class when the page has finished loading (and use the same technique to remove `tbl-col2`'s width beforehand). This still reproduces the flicker in Chrome; I worry that this has gone beyond my question's usage of `display:none` to simulate the loading state, though. – Lightness Races in Orbit Jun 30 '15 at 08:55
  • @LightnessRacesinOrbit I didn't think that while the first cell is being parsed, the spacer is not there yet. The trick is using a CSS pseudo-element. – Oriol Jun 30 '15 at 14:22
  • Seems to do the job! Thanks :-) – Lightness Races in Orbit Jul 01 '15 at 10:00
  • Please note: the column width won't be flexible to content any more. Try to use 'LHSSSSSS' instead of 'LHS'. – skobaljic Jul 02 '15 at 12:49
  • @skobaljic Since the wrapper and each column have a defined width, I think this behavior is desired. However, adding `word-wrap: break-word` may be useful to break long words instead of horizontal overflow. – Oriol Jul 02 '15 at 14:31
  • I also think it may be good for you, just commenting for other visitors. Table won't expand if you use fixed table layout. – skobaljic Jul 02 '15 at 21:14
1

would this be a reasonable fix?

#tbl      { display: table; width: 200px; border: 1px solid black; }
#tbl-row  { display: flex;  }
#tbl-col1,
#tbl-spc,
#tbl-col2 {
   flex:0;
   overflow:hidden
}

#tbl-col1 { flex-basis: 50px;  background-color: red; }
#tbl-col2 { flex-basis: 150px; background-color: blue; }
<div id="tbl">
    <div id="tbl-row">
        <div id="tbl-col1">LHS-LHS-LHS-LHS</div>
        <div id="tbl-spc"></div>
        <div id="tbl-col2">RHS</div>
    </div>
</div>

EDIT:

here's a fiddle with prefixes and fallback

maioman
  • 18,154
  • 4
  • 36
  • 42
  • Yes, this is what I wanted. :) Thanks – Lightness Races in Orbit Jun 27 '15 at 21:47
  • This works only on Chrome as far as I can see, looks broken in FF and IE. Using `inline-block` won't work this way (there is a huge difference between `table-cell` and `inline-block` and I guess browser tries to fix it somehow.. and Chrome won the contest). – skobaljic Jun 27 '15 at 21:51
  • @skobaljic: Aggghhh rats you're right :( Too good to be true! – Lightness Races in Orbit Jun 27 '15 at 21:53
  • Using flex will fix the issue , works in in all browsers and up to ie10 ; [fiddle](https://jsfiddle.net/maio/adbe3z97/4/) – maioman Jun 27 '15 at 23:10
  • `display: table` can be removed. It will just wrap the flex container inside an anonymous table-cell element. That won't hurt but is unnecessary. – Oriol Jun 30 '15 at 14:34
  • @Oriol I fully agree , but LightnessRacesinOrbit could set the original markup as fallback (for ie9 or less) – maioman Jun 30 '15 at 21:04
  • @LightnessRacesinOrbit - this is just a css hack and even if it works flex wasn't conceived for doing this; although Oriol's and Skobaljic's answer involve a change in markup they're just as good; BTW Skobaljic's method of creating an invisible table-header looks cleaner to me – maioman Jul 01 '15 at 12:46
0

Wanna try this - JS Fiddle

.tbl{display:table;}
.fixed{width: 200px; border: 1px solid black; }
.tblRow { display: table-row; }
.tblCol{ display: table-cell; }
.col1 .tbl { width: 50px;  background-color: red; }
.col2 .tbl { width: 150px; background-color: blue; display:none; }

As I said in my comment, Play with the table width. So here it is. Remove display:none from style line .col2 .tbl { width: 150px; background-color: blue; display:none; } and all you can see. this is perhaps what you want.

Raj
  • 570
  • 5
  • 15