Every chart in this project relies solely on semantic markup — <table> based — and a spread of CSS variables carried by the tags. No JavaScript required for display, and styles are progressively enhanced depending on your browser's capabilities.

Note : by virtue of the experimental nature of these techniques and a solid foundation enhanced progressively, I don't mention browser support for each example — but it goes without saying that this is not magic, and only modern browsers handle this right. Other browsers should display a properly styled table, and that's nice.


A major effort has been made to ensure accessibility. As mentioned above, semantic and structured markup is a prerequisite — but it's not enough. CSS is being applied as gradually as possible, in order to guarantee the best possible display of data for each user.

Accessible table

Wakeup call :

For other useful tips, I warmly recommend reading Data Tables Inclusive Component by Heydon Pickering, which is a real gold mine.


To distinguish the different areas other than by colour, an svg pattern is applied in css — you can find some of them on the Hero Patterns website:

  1. in order to improve blending with background colors or gradients, the background-blend-mode property is used with the hard-light value;
  2. pattern's size and position depends directly on the value and scale of the chart, depending on the type of chart;
  3. in order not to embed too many external files, we use a technique proposed by Trys Mudford to include each svg in data URi in a css variable; thus, a finite list of patterns is used in the theme, without ever repeating the svg.

What to encode?

Chris Coyier (via Charlotte Dann) explained that as of now, only the octhotorpe needs to be encoded in css . Don't bother with the other characters, their encoding seriously affects readability.

.chaarts {
	--stripes: url("data:image/svg+xml,<svg width='6' height='6' viewBox='0 0 6 6' xmlns=''><g fill='%23fff9' fill-rule='evenodd'><path d='M5 0h1L0 6V5zM6 5v1H5z'/></g></svg>");

.chaarts tr:nth-child(2n + 2) {
	--background: var(--stripes);

.chaarts td {
	--size: calc( var(--scale, 100) * 100% );
	--position: calc( var(--value, 0) / var(--scale, 100) * 100% );
		linear-gradient( right, Window, ButtonFace, ButtonText, Highlight ) var(--position) 0% / var(--size) 100%,
		var(--background) center / contain;
	background-blend-mode: hard-light;

User preferences

In order to respect as much as possible the preferences of the visitors, many elements have been adapted:

  1. dimensions are in relatives units (em or rem as the case may be), in order to fit coherently with the body of text inherited from the browser and to be able to be enlarged or reduced without loss;
  2. colors are adjusted when High Contrast Mode is detected using prefers-contrast: more.
  3. animations and transitions are disabled when the system exposes this preference through prefers-reduced-motion: reduce ;
  4. hover effects whose initial state consists in hiding content are activated contextually in the @media (hover: hover) { … } media query.
  5. RTL is supported thanks to logical properties and a few tweaks here and there.

display and semantics

Adrian Roselli explains that playing with a <table> or <dl> element's display endangers its semantics. The latter is therefore "locked" using dedicated aria roles — as he explains in a detailed article.

That's why each table is preceded by a switch — based on Heydon Pickering inclusive toggle button — whose one and only role is to disable additional styles:

document.addEventListener("DOMContentLoaded", function() {
	var switches = document.querySelectorAll('[role="switch"]');, function(el, i) {
		el.addEventListener('click', function() {
			var checked = this.getAttribute('aria-checked') == 'true' || false;
			this.setAttribute('aria-checked', !checked);

			var chart = this.parentNode.nextElementSibling;

Well, we're ready to get to the heart of the matter.
Warm up your dev tools!