The Periodic Table in CSS

Mads Stoumann - Sep 8 - - Dev Community

Like the Solar System, The Periodic Table has been done in CSS a lot of times … but it has never been done as simply, as I'm about to show you.

Let's start with some basic, semantic markup:

<ol>
  <li data-mass="1.0078">
    <abbr title="Hydrogen">H</abbr>
  </li>
</ol>
Enter fullscreen mode Exit fullscreen mode

We use an ordered list, <ol>, as this is an ordered system of elements.

We then have a <li> tag for each element, and an <abbr> tag.

The name of the <abbr> element is an abbreviation for the word “abbreviation”, which is cute.
— Heydon Pickering.

Now, instead of Googling the atomic mass of each element, we just ask ChatGPT to fill out the rest of the markup. We also ask it to add a 3-letter class to each element, indicating which type the element is, ie. a "noble gas" (class="nbl") etc. — and we get 118 elements:

<ol>
  <li data-mass="1.0078" class="rnm">
    <abbr title="Hydrogen">H</abbr>
  </li>
  <li data-mass="4.0026" class="nbl">
    <abbr title="Helium">He</abbr>
  </li>
  <li data-mass="6.941" class="alk">
    <abbr title="Lithium">Li</abbr>
  </li> 
  <!-- etc. -->
</ol>
Enter fullscreen mode Exit fullscreen mode

It doesn't look great yet; it's just a numbered list with abbreviations for the elements.

1. H
2. He
3. Li
etc.
Enter fullscreen mode Exit fullscreen mode

Let's turn the list into a 18x10 grid:

ol {
  all: unset;
  container-type: inline-size;
  counter-reset: element;
  display: grid; 
  font-size: 2cqi;
  gap: 1px;
  grid-template-columns: repeat(18, 1fr);
  grid-template-rows: repeat(10, 1fr);
}
Enter fullscreen mode Exit fullscreen mode

Now, we set each <li> to be a square box and create an internal grid to place the atomic number top-left, the mass (data-mass) in the top-right, and the <abbr> tag below:

li {
  aspect-ratio: 1 / 1;
  background: #EEEEEE;
  counter-increment: element;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr 1fr;
  padding: .25ch;
  transition: scale .125s ease-in;
  &::before {
    content: counter(element);
  }
  &::after {
    content: attr(data-mass);
    grid-area: 1 / 2 / 2 / 2;
    justify-self: end;
  }
  &::before, &::after {
    font-size: .33em;
  }
}
Enter fullscreen mode Exit fullscreen mode

Before we see what we've accomplished, let's ask ChatGPT to add some colors to the "element-type"-classes it added earlier. Now we get:

Raw table

Looks great, but not exactly like the periodic table we learned in school. Let's add some grid-magic.

For Helium, we want it to be pushed to the last column. As we know the grid is 18 columns wide, we simply add:

li {
  &:nth-of-type(2) { grid-column: 18; }
}
Enter fullscreen mode Exit fullscreen mode

Since this is an ordered list, the nth-of-type value will always correspond to the atomic number of each element. We want to move Boron and Aluminum to column 13:

li {
  &:nth-of-type(5), &:nth-of-type(13) { grid-column: 13; }
Enter fullscreen mode Exit fullscreen mode

Let's check it out:

Raw table 2

An improvement, for sure, but since grid-column just pushes the grid forward, how can we take elements 58-71 and 90-103 (the lathenides and actinides) completely out of their grid-flow and add them to those 2 rows below the main grid?

For that, we can use grid-area, where we define:

row-start / col-start / row-end / col-end

In our case, that'll be:

li {
   /* Lanthenides */
  &:nth-of-type(58) { grid-area: 9 / 4 / 9/ 4; }
  &:nth-of-type(59) { grid-area: 9 / 5 / 9/ 5; }
  &:nth-of-type(60) { grid-area: 9 / 6 / 9/ 6; }
  /* etc. */

  /* Actinides */
  &:nth-of-type(90) { grid-area: 10 / 4 / 10 / 4; }
  &:nth-of-type(91) { grid-area: 10 / 5 / 10 / 5; }
  &:nth-of-type(92) { grid-area: 10 / 6 / 10 / 6; }
  / etc. */
}
Enter fullscreen mode Exit fullscreen mode

And now we get (for clarity, I've enabled Dev Tools' grid-visualizer):

Periodic table with grid-preview

Notice how the grid-elements after the element we've moved out of the flow, continue in the main flow!


Filtering

Now, let's use these "element type"-classes, we had ChatGPT generate earlier, to filter the periodic table.

First, let's add some basic HTML:

<fieldset>
  <legend>Filter</legend>
  <label>
    <input type="radio" id="alk" name="filter">
    Alkali Metals
  </label>
</fieldset>
Enter fullscreen mode Exit fullscreen mode

Then, we ask chatGPT to fill out the rest, and add an "All"-option with no id:

Filter

We need a bunch of JavaScript to filter, right? No, we can do this in plain CSS:

body:has(#alk:checked) li:not(.alk) { 
  opacity: 0.2;
}
Enter fullscreen mode Exit fullscreen mode

The logic works like this: If the body contains a checkbox with the id="alk" and it's checked, the styles will be applied to all <li> elements that don’t have the .alk class.

Repeat for all the types and classes.

Let's click on "metalloids":

Metalloids

How cool is that?


That concludes this tutorial … but wait … what does that Heisenberg filter do? It wasn't in the list of filters from ChatGPT?

Let's click it:

Heisenberg

… and now you know my favorite TV-show of all time!

Demo

Here's a Codepen — even though it's fully responsive, I recommend viewing it on larger screens:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player