Make the rows banded if and only if there are at least 4 rows

That was a practical problem I encountered in 2016 when I first developed my blog site. I insisted on implementing it with pure CSS as long as it was possible. Banded rows itself is simple, but later, for certain reasons I wanted to make the rows banded if and only if there are at least 4 rows, and finally I reached the solution with some mathematical logic tricks.

Updated on 26 May 2017: Another idea just stroke me, which is much simpler.

Why should you make your tables banded?

Banded tables are more stylish. That’s one point. Moreover, considering the accessibility, I bet that everyone has the experience of failing to read a wide table in Excel if it is in its plain style. Your eyes just cannot keep aligned to the row of your interest. Skipping a line also happens when reading a wide column of text, which really happened before I added multiple-column styling for this site (another way could have been limiting the maximum width of the content area).

How can we avoid losing track of a row? In Excel, one can use the arrow keys to navigate around to make sure the focused row is not altered. However, this approach is somehow unfeasible for tables on a web page because one cannot key-navigate a web page — unless with a screen reader, in which case there is no need to style things at all.

An idea is to make the tables banded. This is actually giving the user a visual checksum (the parity).

Making the rows banded with CSS

It is quite straightforward that we want to make, say, odd rows, have a darker background. I, with little CSS experience by then, looked up MDN and found the documentation of :nth-child pseudo-class. So one can write:

table > tbody > tr:nth-child(odd)
{
    background: rgba(0, 0, 0, 0.02);
}

My site has two non-High Contrast themes, light and dark. The odd rows always have the background tweaked, i.e., darker under the light theme and lighter under the dark theme.

Reducing clutter

Perhaps I should have made even rows tweaked because it is somehow strange to me to see a one-row table with a non-trivial background. It is also unnecessary to have the table banded if it has only two rows because the user can simply remember whether the row of interest is the top row or the bottom row. Actually, even a table with 3 rows doesn’t need this fancy style.

Not banding a table reduces clutter. Different appearances among rows give extra information, which can be unwanted. The effect can be subconscious. Well, I haven’t conducted research in this area.

I think this has something to do with subitizing (the ability to tell the number of objects at a glance when the number is small). Wikipedia says average people can tell up to 4 objects at a glance. In 2008, an MIT-lead team found a language without numbers. Long story shortened, the language uses one word for 1 to 4 objects, another word for 5 to 6 objects and a third word for ‘many’ objects. There are a bunch of researches of related phenomena done by scientists.

People can tell up to 4 objects at a glance and I assume people can track one of those objects easily. This is not a logical deduction but just a vague conjecture by me.

For some reason, I decided to make a table banded if and only if it has at least 4 rows. Don’t you go into all details of my decision because it is quite arbitrary! I made it in the hope that the UI would be balanced in terms of checksum information conveyed visually. Now, read the next part of this blog entry.

Adding the condition ‘at least 4 rows’

The first problem is how one decides whether a table has at least 4 rows. The answer is that one cannot do this with a CSS selector. Or can one? Tell me if so.

Just adding another thing to make it harder. I use a Markdown compiler to build my blog site, so it is possible to add special logic to the compiler on generating table tags (it could add class="gl-banded-table" conditionally). However, this is not a ‘pure’ solution because what I want is to style the tables by and only by its number of rows, and I will have to regenerate the whole site if I just want to change the threshold (4) of banded tables. It is also possible to attach a cute script tag at the end of each entry page that detects tables with at least 4 rows and adds a proper class when necessary. However, this fails another requirement (or wish) of the site (that was later added as a requirement or wish, but that’s another story) — the site should fall back gracefully without JavaScript. JavaScript-less environment includes Safari (it can be turned off) and Internet Explorer (using security settings). Anyway, let’s just do it with pure CSS.

The obvious idea is to test the children trs with appropriate selectors.

Detecting if an element is… by…
the only child :only-child
the first child of 2 :first-child:nth-last-child(2)
the second child of 2 :nth-child(2):last-child
the k-th child of 3 :nth-child(k):nth-last-child(4 - k)

Note that we actually won’t need to detect the second children because they are already unselected by :nth-child(odd).

It is also easy to discover :not pseudo-class selector. Putting these all together, we want to style any tr selected by the following imaginary selector.

/* It is sad that I cannot add space characters because that would change the selector. */
table
> tbody
> tr:nth-child(odd):not(:only-child):not(:first-child:nth-last-child(2)):not(:first-child:nth-last-child(3)):not(:last-child:nth-child(3))

Exercise Why can’t I style table > tbody > tr:nth-child(odd) and then unstyle the four unwanted cases by styling again with background: none;?

Answer This is not beautiful. And it can be troublesome if one day I add special style to some rows — I will have to know the specificity of other selectors selecting to the unstyled row. Never unstyle something by ‘styling it back’, which is almost always a terrible idea. It is actually impossible to ‘unstyle’ an element because you cannot predict what style is waiting at the next position. For example, say one wants to style .button with border: 1px solid; but undo the style if it fits some other condition X. It is wrong to style .button with border: 1px solid;, then X.button with border: none;, which could accidentally remove other border styles that would be applied if neither were present. It also adds some curiosity to the pursuit of the ‘pure’ implementation of the wanted style.

Let’s get back to the thing. Why did I say imaginary? Because the magical :not only negates one condition — :not(.class1.class2) was simply wrong for the CSS standards then (and also is now, by the time this entry was written). In 2016, I did some searching before I realised there were nothing I could do to make :not (or any other selector with negation functionality) negate a conjunction.

The solution

Suppose we have two Boolean formulae XX and YY. What is equivalent to ¬(XY)\neg\left({X\wedge Y}\right)? The negation of a conjunction is a disjunction of the negations — the answer is ¬X¬Y\neg X\vee\neg Y. Combining it with the distributivity of disjunction to conjuctions, one finds that :not(XY):not(ZW) is:

Suppose we have two selectors X and Y. What is equivalent to :not(XY)? The negation of a conjuction is a disconjunction of the negations — the answer is :not(X), :not(Y). Combining it with the distributivity of disjuction to conjunctions, one finds that :not(XY):not(ZW) is:

:not(X):not(Z), :not(X):not(W),
:not(Y):not(Z), :not(Y):not(W)

It is already cumbersome to expand the imaginary selector into a disjunction of 8 selectors. But equipped with that transformation rule, one can write a preprocessor for that. The code used in the site (by the time of writing of this entry) for the light theme was:

table > tbody > tr:nth-child(odd):not(:only-child):not(:first-child):not(:first-child):not(:last-child),
table > tbody > tr:nth-child(odd):not(:only-child):not(:nth-last-child(2)):not(:first-child):not(:last-child),
table > tbody > tr:nth-child(odd):not(:only-child):not(:first-child):not(:nth-last-child(3)):not(:last-child),
table > tbody > tr:nth-child(odd):not(:only-child):not(:nth-last-child(2)):not(:nth-last-child(3)):not(:last-child),
table > tbody > tr:nth-child(odd):not(:only-child):not(:first-child):not(:first-child):not(:nth-child(3)),
table > tbody > tr:nth-child(odd):not(:only-child):not(:nth-last-child(2)):not(:first-child):not(:nth-child(3)),
table > tbody > tr:nth-child(odd):not(:only-child):not(:first-child):not(:nth-last-child(3)):not(:nth-child(3)),
table > tbody > tr:nth-child(odd):not(:only-child):not(:nth-last-child(2)):not(:nth-last-child(3)):not(:nth-child(3))
{
    background: rgba(0, 0, 0, 0.02);
}

The process is actually transforming a boolean expression into its disjunctive normal form (not necessarily the minimal sum form).

Updated on 26 May 2017: A much simpler solution can be found here.

Remarks

By the time of writing of this entry, I found few, if any, discussions on the Internet regarding this issue. There were many discussions on negating a disjunction. Negating a conjunction is costly because the resulting CSS might have exponential length while negating a disjunction increases the length of ideal form the selector linearly.

CSS Level 4 allows a selector list as the argument of :not, which is unfortunately not found to be supported by any browser at this time. And before CSS Level 4 become popular, we still have to do the transformation ourselves.

Updated on 26 May 2017

Were you fascinated by the solution provided above, you would find yourself as stupid as I was. There is actually a much simpler solution for this scenario.

We only want to select odd trs that are inside a tbody with at least 4 trs. Earlier we tried our best to negate the selection by disselecting a tr if it is the only child, the first of two children or the first or the third of three children. However, we can classify the trs of our interest into the following categories:

  • The fifth, senventh, … child.
  • The first child that is not the last, the second last or the third last child.
  • The third child that is not the last child.

Therefore the following should be enough:

table > tbody > tr:nth-child(2n+5),
table > tbody > tr:first-child:not(:nth-last-child(-n+3)),
table > tbody > tr:nth-child(3):not(:last-child)
{
    background: rgba(0, 0, 0, 0.02);
}

In case one has OCD, it is also convenient to make the selectors have the same specificity by changing the first to table > tbody > tr:nth-child(2n+5):not(:first-child) or something similar.

Please enable JavaScript to view the comments powered by Disqus.