Christian Heilmann

Zebra tables using nth-child and hidden rows?

Thursday, December 12th, 2013 at 3:39 am

lion dressed as zebra

Earlier today my colleague Anton Kovalyov who works on the Firefox JavaScript Profiler ran across an interesting problem: if you have a table in the page and you want to colour every odd row differently to make it easier to read (christened as “Zebra Tables” by David F. Miller on Alistapart in 2004) the great thing is that we have support for :nth-child in browsers these days. Without having to resort to JavaScript, you can stripe a table like this.

Now, if you add a class of “hidden” to a row (or set its CSS property of display to none), you visually hide the element, but you don’t remove it from the document. Thus, the striping is broken. Try it out by clicking the “remove row 3” button:

The solution Anton found other people to use is a repeating gradient background instead:

This works to a degree but seems just odd with the fixed size of line-height (what if one table row spans more than one line?).

Jens Grochtdreis offered a pure CSS solution that on the first glance seems to do the job using a mixture of nth-of-type, nth-child and the tilde selector.

This works, until you remove more than one row. Try it by clicking the button again. :(

In essence, the issue we are facing here is that hiding something doesn’t remove it from the DOM which can mess with many things – assistive technology, search engines and, yes, CSS counters.

The solution to the problem is not to hide the parts you want to get rid of but really, physically remove them from the document, thus forcing the browser to re-stripe the table. Stuart Langridge offered a clever solution to simply move the table rows you want to hide to the end of the table using appendChild() and hide them by storing their index (for later retrieval) in an expando:

[…]rows[i].dataset.idx=i;table.appendChild(rows[i]) and [data-idx] { display:none }

That way the original even/odd pairs will get reshuffled. My solution is similar, except I remove the rows completely and store them in a cache object instead:

This solution also takes advantage of the fact that the result of querySelectorAll is not a live list and thus can be used as a copy of the original table.

As with everything on the web, there are many solutions to the same problem and it is fun to try out different ways. I am sure there is a pure CSS solution. It would also be interesting to see how the different ways perform differently. With a huge dataset like the JS Profiler I’d be tempted to guess that using a full table instead of a table scrolling in a viewport with recycling of table rows might actually be a bottleneck. Time will tell.

Share on Mastodon (needs instance)

Share on BlueSky

Newsletter

Check out the Dev Digest Newsletter I write every week for WeAreDevelopers. Latest issues:

Word is Doomed, Flawed LLM benchmarks, hard sorting and CSS mistakes Spot LLM benchmark flaws, learn why sorting is hard, how to run Doom in Word and how to say "no" like a manager.
30 years of JS, Browser AI, how attackers use GenAI, whistling code Learn how to use AI in your browser and not on the cloud, why AI makes different mistakes than humans and go and whistle up some code!
197: Dunning-Kruger steroids, state of cloud security, puppies>beer
196: AI killed devops, what now? LLM Political bias & AI security Learn how AI killed DevOps, create long tasks in JS, why 1 in 5 security breaches are AI generated code & play "The Scope Creep"
195: End of likes, JS Zoo and Tim Berners-Lee doesn't see AI vs Web Meta kills like buttons, Tim-Berners-Lee thinks AI won't kill the web, GitHub is ending toasts and the worst selling Microsoft product.

My other work: