Glassmorphism Periodic Table

Build a dynamic periodic table with frosted-glass CSS effects.

Introduction

30 Second Summary

The periodic table is one of the most iconic data visualizations in science. Turning it into a polished, interactive webpage is a perfect challenge for combining layout skills with modern visual design.

In this project, you will build a glassmorphism-styled periodic table that fetches all 118 elements from an open-source dataset and renders them as frosted-glass cards in an 18-column CSS Grid layout, color-coded by element category.

What You'll Build

A periodic table webpage where JavaScript fetches element data from a JSON file and dynamically renders 118 frosted-glass tiles into a CSS Grid that matches the real periodic table structure.

By the end of this project, you'll have:

  • A fully rendered periodic table displaying all 118 elements with atomic number, symbol, name, atomic mass, and electron configuration on every tile.
  • A glassmorphism-styled webpage using CSS backdrop-filter, semi-transparent backgrounds, and layered visual effects.
  • A working understanding of CSS Grid for complex, non-uniform layouts including lanthanide and actinide rows.
  • Secret Mission: Add a click-to-expand detail modal with glassmorphism styling that shows additional element info like phase, density, and discovery history.

Are there any prerequisites?

No prior experience is needed. This guide walks you through setting up all required tools from scratch, including VS Code and the Live Server extension.

Before We Start

To build a glassmorphism periodic table, you need three files working together: an HTML file for structure, a CSS file for the frosted-glass styling, and a JavaScript file that fetches element data from the internet.

JavaScript's fetch() function is blocked by browsers when opening files directly from your computer. You need a local development server to load the page properly. In this step, you'll install a server extension in VS Code and create your project files.

In this step, get ready to:
  • Install the Live Server extension in VS Code.
  • Create the periodic-table project folder with three files.
  • Verify Live Server opens your project in Chrome.
Install the Live Server extension

Live Server gives you a local HTTP server that automatically refreshes your browser whenever you save a file. Without it, your browser would block the fetch() call you'll write later.

  • Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type Live Server in the search bar at the top of the panel.
  • Find the result by Ritwick Dey and click Install.

What is Live Server?

Live Server launches a mini web server on your computer that serves your files over HTTP (just like a real website). It also watches your files for changes and automatically refreshes the browser when you save.

Don't see the Install button?

If you see Uninstall instead of Install, the extension is already installed. You're good to move on.

If no results appear, make sure you spelled it correctly and check your internet connection. Help me troubleshoot my Live Server installation.

Create the project folder and files

Your project needs a dedicated folder to keep the HTML, CSS, and JavaScript files together. You'll create this on your Desktop so it's easy to find.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal (macOS) or PowerShell (Windows), and pressing Enter.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir periodic-table
cd periodic-table
  • Create the three project files by running this command:

macOS

touch index.html style.css script.js

Windows

New-Item index.html, style.css, script.js
  • Confirm the files exist by running:
ls

You should see index.html, style.css, and script.js in the output.

Don't see all three files?

Make sure you're inside the periodic-table folder when you run ls. If you see a different directory's contents, run cd ~/Desktop/periodic-table and try again.

Help me create my project files.

Open the project in VS Code and verify Live Server

Now you'll open the folder in VS Code and confirm that Live Server can serve your files to the browser.

  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the periodic-table folder, and click Open.
  • In the Explorer sidebar on the left, right-click index.html.
  • Click Open with Live Server.

Your default browser should open showing a blank page. This is expected because index.html is currently empty.

Why is the page blank?

An empty HTML file still loads successfully in the browser. The important thing is that the URL bar shows something like 127.0.0.1:5500 instead of a file:// path. This means Live Server is working and serving your files over HTTP.

Don't see Open with Live Server?

Make sure the Live Server extension finished installing. Look for a Go Live button in the VS Code status bar at the bottom of the window. If it's missing, restart VS Code and check the Extensions panel again.

Help me get Live Server working in VS Code.

Your workspace is ready. Next up, you'll fetch real periodic table data from the internet and render all 118 elements on screen.

Set Up Project Files

Your project folder is ready with three empty files waiting to be filled. Before you can fetch element data or apply glassmorphism effects, you need an HTML skeleton that ties everything together.

In this step, you'll add the foundational HTML structure to index.html and launch a local development server. This lets you preview changes in real time as you build out the periodic table.

In this step, get ready to:
  • Add HTML boilerplate to index.html with the page structure and linked assets.
  • Launch Live Server to preview the page in Chrome.
Add the HTML boilerplate

Every web project starts with an HTML file that links your stylesheet and script. This file also defines the containers that JavaScript will later fill with element data.

  • In VS Code, click on index.html in the Explorer sidebar to open it in the editor.
  • Replace everything in the file with the following code:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Glassmorphism Periodic Table</title>
    <!-- Link to the external stylesheet -->
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <!-- Page heading -->
    <h1>Periodic Table of Elements</h1>

    <!-- Container for the periodic table grid -->
    <div class="table-container">
        <div class="periodic-table" id="periodic-table"></div>
    </div>

    <!-- Legend container for element category colors -->
    <div class="legend" id="legend"></div>

    <!-- Link to the JavaScript file at the end of body -->
    <script src="script.js"></script>
</body>
</html>

What does this code do?

  • The <link> tag connects style.css so the browser can apply your styles.
  • The #periodic-table div is an empty container. JavaScript will inject all 118 element tiles into it later.
  • The #legend div will hold color-coded swatches identifying each element category.
  • The <script> tag is placed at the end of the body so the HTML loads before JavaScript tries to access it.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).

File not saving?

Make sure you're editing the correct file. Check the tab at the top of the editor. It should say index.html.

If you see a white dot on the tab, the file has unsaved changes. Press the save shortcut again.

Why isn't my file saving in VS Code?

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete index.html file for comparison:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Glassmorphism Periodic Table</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>Periodic Table of Elements</h1>
    <div class="table-container">
        <div class="periodic-table" id="periodic-table"></div>
    </div>
    <div class="legend" id="legend"></div>
    <script src="script.js"></script>
</body>
</html>
Launch Live Server

Opening the HTML file directly in a browser uses the file:// protocol, which blocks JavaScript fetch() calls. Live Server spins up a local HTTP server that avoids this restriction and auto-refreshes the page when you save.

  • In the VS Code Explorer sidebar, right-click on index.html.
  • Select Open with Live Server.

Google Chrome opens automatically and navigates to a localhost address. You should see a mostly blank white page with the heading "Periodic Table of Elements" displayed in large default text at the top.

Don't see the heading?

Make sure you saved index.html before launching Live Server. Unsaved changes won't appear in the browser.

If the browser opens but shows a file listing instead of your page, make sure you right-clicked index.html specifically, not the folder.

My Live Server page isn't showing the heading from my HTML file. How do I fix this?

Why use Live Server?

Later in this project, your JavaScript will use fetch() to load element data from an external URL. Browsers block these network requests on the file:// protocol for security reasons.

Live Server provides a proper HTTP server so your fetch() calls work without issues. As a bonus, it refreshes the page every time you save a file.

Your HTML skeleton is wired up and Live Server is running. Next up, you'll write JavaScript to fetch all 118 elements from a public dataset and render them on the page.

Fetch and Render Element Data

Your project folder is set up and Live Server is serving a blank page in Chrome. Now it's time to bring that page to life with real element data.

You'll use JavaScript's fetch API to pull all 118 elements from an open-source JSON dataset hosted on GitHub. Each element comes with its atomic number, symbol, name, mass, electron configuration, and grid coordinates that tell you exactly where to place it on the table.

In this step, get ready to:
  • Fetch the full periodic table dataset from GitHub.
  • Build a function that creates a tile for each element.
  • Render all 118 elements onto the page.
Fetch the element data from GitHub

With 118 elements to display, hand-coding HTML for each one would be impractical. Instead, you'll fetch a pre-built dataset that includes every element's properties and its exact position on the periodic table grid.

  • In VS Code, click on script.js in the Explorer sidebar to open the empty file.
  • Add the following code to fetch the periodic table JSON and log it to the console:
const DATA_URL = "https://raw.githubusercontent.com/Bowserinator/Periodic-Table-JSON/master/PeriodicTableJSON.json";

async function init() {
    try {
        const response = await fetch(DATA_URL);
        const data = await response.json();
        // Log the elements to verify the data loaded
        console.log(data.elements);
    } catch (error) {
        console.error("Failed to load periodic table data:", error);
    }
}

init();

What does this code do?

DATA_URL stores the URL of an open-source JSON file containing data for all 118 elements. The dataset includes properties like atomic number, symbol, mass, and even grid coordinates for layout.

The init() function uses fetch() to download the JSON, parses it into a JavaScript object, and logs the elements array so you can verify the data loaded. The try/catch block handles network errors gracefully.

  • Save script.js.
  • Switch to Chrome and open DevTools by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows).
  • In the Console tab, you should see an array of 118 objects logged. Click the arrow next to the array to expand it and inspect individual elements.

Not seeing anything in the console?

  • Make sure Live Server is still running. If it stopped, right-click index.html and select Open with Live Server again.
  • Check that your index.html file has <script src="script.js"></script> before the closing </body> tag.
  • If you see a red error mentioning CORS or network failure, confirm you're viewing the page through Live Server (the URL should start with http://127.0.0.1) and not as a file:// path.

Why isn't my console.log showing any data?

Build the element tile function

Each element in the JSON has a category field like "alkali metal" or "noble gas". You'll map these categories to CSS class names so you can style each group differently later.

  • In script.js, place this category mapping code directly below the DATA_URL line (above the init() function):
const CATEGORY_CLASSES = {
    "diatomic nonmetal": "diatomic-nonmetal",
    "noble gas": "noble-gas",
    "alkali metal": "alkali-metal",
    "alkaline earth metal": "alkaline-earth-metal",
    "metalloid": "metalloid",
    "polyatomic nonmetal": "polyatomic-nonmetal",
    "post-transition metal": "post-transition-metal",
    "transition metal": "transition-metal",
    "lanthanide": "lanthanide",
    "actinide": "actinide"
};

function getCategoryClass(category) {
    if (!category) return "unknown";
    for (const [key, value] of Object.entries(CATEGORY_CLASSES)) {
        if (category.includes(key)) return value;
    }
    return "unknown";
}

What does this code do?

CATEGORY_CLASSES maps human-readable category names from the JSON (like "alkali metal") to CSS-friendly class names (like "alkali-metal").

getCategoryClass() uses includes() instead of strict equality because some elements in the JSON have longer category strings like "unknown, probably transition metal". The includes() check still matches the core category name within those longer strings.

You won't see a visible change yet. The next chunk uses this mapping to build each element tile.

Now you'll write the function that constructs a DOM element for each tile. Each tile displays five pieces of data and uses inline grid positioning from the JSON.

  • Add the createElement function below getCategoryClass and above init():
function createElement(element) {
    const div = document.createElement("div");
    const categoryClass = getCategoryClass(element.category);
    div.className = `element ${categoryClass}`;
    // Position on the grid using coordinates from the JSON
    div.style.gridColumn = element.xpos;
    div.style.gridRow = element.ypos;

    div.innerHTML = `
        <span class="atomic-number">${element.number}</span>
        <span class="symbol">${element.symbol}</span>
        <span class="name">${element.name}</span>
        <span class="mass">${element.atomic_mass.toFixed(2)}</span>
        <span class="electron-config">${element.electron_configuration_semantic}</span>
    `;

    return div;
}

What does this code do?

  • Creates a div and assigns it the class element plus its category class (e.g. noble-gas).
  • Sets gridColumn and gridRow using the JSON's xpos (1-18) and ypos (1-10) fields. This places each element in its correct periodic table position without you needing to calculate coordinates.
  • Fills the div with five spans displaying atomic number, symbol, name, atomic mass (rounded to 2 decimals), and electron configuration.

You won't see a visible change yet. The next chunk connects this function to your data loop.

Render all elements to the page

Your createElement function is ready but nothing calls it yet. You'll update init() to loop through every element in the dataset and append tiles to the page.

  • In script.js, find your existing init() function.
  • Replace it entirely (including the init(); call at the bottom) with the updated version below. Also add the createLegend() stub directly above init():
function createLegend() {
    const legend = document.getElementById("legend");
}

async function init() {
    try {
        const response = await fetch(DATA_URL);
        const data = await response.json();
        const table = document.getElementById("periodic-table");

        // Loop through all elements and add each tile to the grid
        data.elements.forEach(element => {
            const el = createElement(element);
            table.appendChild(el);
        });

        createLegend();
    } catch (error) {
        console.error("Failed to load periodic table data:", error);
        document.getElementById("periodic-table").innerHTML =
            '<p style="color: white; grid-column: 1 / -1;">Failed to load element data. Please check your internet connection.</p>';
    }
}

init();

What's happening here?

  • createLegend() is a placeholder stub. You'll fill it in during a later step when you add the color-coded legend.
  • The updated init() grabs the #periodic-table container, loops through all 118 elements, calls createElement() for each one, and appends the resulting div to the page.
  • The catch block now shows a user-friendly error message on the page if the fetch fails, instead of only logging to the console.
  • Save script.js.
  • Switch to Chrome. Live Server should auto-refresh the page. You should see 118 unstyled text blocks scattered across the page in roughly the correct periodic table positions.

Page is still blank or shows an error?

  • Open DevTools (Cmd+Option+J / Ctrl+Shift+J) and check the Console tab for red errors. A typo in a function name or missing closing bracket will show up here.
  • Make sure createLegend() is defined above init(). JavaScript needs to see the function declaration before it's called.
  • Confirm the old console.log(data.elements) line is gone. You should have the forEach loop in its place.

Help me debug why my periodic table elements aren't rendering.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

Here's the complete script.js at the end of this step:

const DATA_URL = "https://raw.githubusercontent.com/Bowserinator/Periodic-Table-JSON/master/PeriodicTableJSON.json";

const CATEGORY_CLASSES = {
    "diatomic nonmetal": "diatomic-nonmetal",
    "noble gas": "noble-gas",
    "alkali metal": "alkali-metal",
    "alkaline earth metal": "alkaline-earth-metal",
    "metalloid": "metalloid",
    "polyatomic nonmetal": "polyatomic-nonmetal",
    "post-transition metal": "post-transition-metal",
    "transition metal": "transition-metal",
    "lanthanide": "lanthanide",
    "actinide": "actinide"
};

function getCategoryClass(category) {
    if (!category) return "unknown";
    for (const [key, value] of Object.entries(CATEGORY_CLASSES)) {
        if (category.includes(key)) return value;
    }
    return "unknown";
}

function createElement(element) {
    const div = document.createElement("div");
    const categoryClass = getCategoryClass(element.category);
    div.className = `element ${categoryClass}`;
    div.style.gridColumn = element.xpos;
    div.style.gridRow = element.ypos;

    div.innerHTML = `
        <span class="atomic-number">${element.number}</span>
        <span class="symbol">${element.symbol}</span>
        <span class="name">${element.name}</span>
        <span class="mass">${element.atomic_mass.toFixed(2)}</span>
        <span class="electron-config">${element.electron_configuration_semantic}</span>
    `;

    return div;
}

function createLegend() {
    const legend = document.getElementById("legend");
}

async function init() {
    try {
        const response = await fetch(DATA_URL);
        const data = await response.json();
        const table = document.getElementById("periodic-table");

        data.elements.forEach(element => {
            const el = createElement(element);
            table.appendChild(el);
        });

        createLegend();
    } catch (error) {
        console.error("Failed to load periodic table data:", error);
        document.getElementById("periodic-table").innerHTML =
            '<p style="color: white; grid-column: 1 / -1;">Failed to load element data. Please check your internet connection.</p>';
    }
}

init();

All 118 elements are on the page, but they look like a wall of unstyled text. Next up, you'll use CSS Grid to transform that text into a proper 18-column periodic table layout with readable tiles.

Build the CSS Grid Layout

Your JavaScript is fetching all 118 elements and dropping them onto the page, but right now they look like unstyled text scattered in random positions.

The periodic table has a precise 18-column structure where every element sits in a specific group and period. In this step, you will use CSS Grid to create that layout and style the element tiles so each one displays readable data.

In this step, get ready to:
  • Set up a dark gradient background and 18-column CSS Grid container.
  • Style the element tiles with flexbox for internal alignment.
  • Size and color the text spans for visual hierarchy.
Set up the page background and grid container

A CSS Grid needs two things: a defined number of columns and rows, and elements placed into those cells. Your JavaScript already assigns each element a gridColumn and gridRow from the JSON data. You just need to define the grid itself.

  • Switch back to style.css in VS Code and add the page-level styles by pasting this code:
/* Remove default browser spacing */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* Dark gradient background fills the viewport */
body {
    min-height: 100vh;
    background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    padding: 20px;
    overflow-x: auto;
    position: relative;
}

/* Page heading with a subtle glow */
h1 {
    text-align: center;
    color: #ffffff;
    margin-bottom: 20px;
    font-size: 2rem;
    text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
    position: relative;
    z-index: 1;
}

What does this code do?

The * selector resets all elements to have zero margin and padding. box-sizing: border-box makes sure padding never pushes elements larger than their defined size.

The body gets a dark purple gradient that sweeps diagonally across the full viewport height. overflow-x: auto allows horizontal scrolling because an 18-column grid is wider than most screens.

  • Below the h1 rule, add the grid container styles:
/* Wrapper allows horizontal scroll on small screens */
.table-container {
    overflow-x: auto;
    padding-bottom: 20px;
    position: relative;
    z-index: 1;
}

/* 18-column grid matching the periodic table structure */
.periodic-table {
    display: grid;
    grid-template-columns: repeat(18, 100px);
    grid-template-rows: repeat(10, 120px);
    gap: 4px;
    width: fit-content;
    margin: 0 auto;
}

Why 18 columns and 10 rows?

The periodic table has 18 groups (columns). The 10 rows cover 7 main periods, a separator gap, and 2 rows for the lanthanides and actinides at the bottom.

Each cell is 100px wide and 120px tall. The JSON data provides xpos (1-18) and ypos (1-10) values for every element, which your JavaScript already maps to gridColumn and gridRow.

  • Save style.css and check your browser.
  • You should see the page turn dark purple with white text arranged in a wide grid pattern. The elements are in roughly the right positions but look like bare text floating on a dark background.

Still seeing a white page?

Make sure your style.css file is saved. Check that the <link rel="stylesheet" href="style.css"> tag in index.html matches your CSS filename exactly.

help me debug why my CSS file isn't loading in the browser

Style the element tiles with flexbox

Each element tile contains five pieces of data stacked vertically: atomic number, symbol, name, mass, and electron configuration. Flexbox in column direction handles this stacking and centering.

  • Below the .periodic-table rule, add the element tile styles:
/* Each element tile uses flexbox to stack content vertically */
.element {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 6px 4px;
    border-radius: 10px;
    cursor: pointer;
    position: relative;
}

What does this code do?

flex-direction: column stacks the five text spans top to bottom. align-items: center and justify-content: center center the content both horizontally and vertically within the cell.

The border-radius: 10px gives each tile rounded corners, which will look great once the glassmorphism effects are added in the next step.

  • Save style.css and check the browser.
  • Each element's text should now be neatly centered within its grid cell instead of being left-aligned.

Elements still look left-aligned?

Make sure you typed .element with the dot prefix. Without the dot, CSS looks for an HTML tag called element instead of a class.

help me figure out why my element tiles aren't centering their content

Style the text spans for visual hierarchy

Five data points need to fit on a tile that is 100px wide and 120px tall. The symbol gets the largest font as the visual anchor, while secondary info like mass and electron configuration stays small and semi-transparent.

  • Below the .element rule, add the text span styles:
/* Atomic number in the top-left corner */
.element .atomic-number {
    font-size: 10px;
    color: rgba(255, 255, 255, 0.7);
    align-self: flex-start;
    margin-left: 4px;
}

/* Element symbol is the visual anchor */
.element .symbol {
    font-size: 22px;
    font-weight: bold;
    color: #ffffff;
    line-height: 1.2;
}

/* Element name sits below the symbol */
.element .name {
    font-size: 8px;
    color: rgba(255, 255, 255, 0.85);
    text-align: center;
    line-height: 1.2;
}

/* Atomic mass in subtle text */
.element .mass {
    font-size: 8px;
    color: rgba(255, 255, 255, 0.6);
}

/* Electron configuration at the smallest size */
.element .electron-config {
    font-size: 7px;
    color: rgba(255, 255, 255, 0.55);
    text-align: center;
    margin-top: 2px;
    word-break: break-all;
}

What does this code do?

Each span targets a different class that your JavaScript already applies in the createElement() function. The symbol at 22px bold is the first thing your eye lands on.

The rgba(255, 255, 255, ...) colors use decreasing opacity for less important data. This creates a natural reading hierarchy without needing different font colors.

  • Save style.css and refresh your browser.
  • You should see a full periodic table with 18 columns of element tiles. Each tile shows a large bold symbol (like H or He) with smaller text for the name, number, mass, and electron configuration.

Text is overlapping or cut off?

Check that you used .element .symbol (with a space between), not .element.symbol (no space). The space means "a .symbol inside a .element" which is what you want.

help me fix overlapping text in my periodic table element tiles

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css before moving on.

ⓧ I'd like to double check the full code

Here is the complete style.css at this point:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    min-height: 100vh;
    background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    padding: 20px;
    overflow-x: auto;
    position: relative;
}

h1 {
    text-align: center;
    color: #ffffff;
    margin-bottom: 20px;
    font-size: 2rem;
    text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
    position: relative;
    z-index: 1;
}

.table-container {
    overflow-x: auto;
    padding-bottom: 20px;
    position: relative;
    z-index: 1;
}

.periodic-table {
    display: grid;
    grid-template-columns: repeat(18, 100px);
    grid-template-rows: repeat(10, 120px);
    gap: 4px;
    width: fit-content;
    margin: 0 auto;
}

.element {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 6px 4px;
    border-radius: 10px;
    cursor: pointer;
    position: relative;
}

.element .atomic-number {
    font-size: 10px;
    color: rgba(255, 255, 255, 0.7);
    align-self: flex-start;
    margin-left: 4px;
}

.element .symbol {
    font-size: 22px;
    font-weight: bold;
    color: #ffffff;
    line-height: 1.2;
}

.element .name {
    font-size: 8px;
    color: rgba(255, 255, 255, 0.85);
    text-align: center;
    line-height: 1.2;
}

.element .mass {
    font-size: 8px;
    color: rgba(255, 255, 255, 0.6);
}

.element .electron-config {
    font-size: 7px;
    color: rgba(255, 255, 255, 0.55);
    text-align: center;
    margin-top: 2px;
    word-break: break-all;
}

Your periodic table now has the correct structure with readable text on every tile. Next up, you will add glassmorphism effects to turn those plain tiles into frosted-glass cards that glow and lift on hover.

Apply Glassmorphism Effects

Your periodic table has the right layout and readable text. The tiles are positioned correctly across 18 columns, but they look flat and plain against the dark background.

In this step, you will transform those flat tiles into frosted-glass cards using a design technique called glassmorphism. This effect layers semi-transparent backgrounds with backdrop blur to create the appearance of frosted glass.

In this step, get ready to:
  • Add glassmorphism properties to each element tile.
  • Create a hover animation that lifts tiles on mouseover.
  • Add decorative background shapes for the blur to refract through.
Add glassmorphism properties to the element tiles

Glassmorphism needs two things working together: a semi-transparent background on the element, and a backdrop-filter that blurs whatever sits behind it. Without the transparent background, the blur has nothing to show through.

  • In style.css, find the .element rule and locate the line that says border-radius: 10px;.
  • Add the following glassmorphism properties directly below it:
    background: rgba(255, 255, 255, 0.08);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.15);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);

What do these properties do?

  • background: rgba(255, 255, 255, 0.08) sets the tile to 8% white opacity. This is nearly transparent, letting the background show through.
  • backdrop-filter: blur(12px) blurs whatever is behind the tile by 12 pixels, creating the frosted glass look.
  • -webkit-backdrop-filter: blur(12px) is the same effect but with a prefix for Safari compatibility.
  • border: 1px solid rgba(255, 255, 255, 0.15) adds a subtle white edge that mimics light catching the glass.
  • box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2) adds depth by casting a soft shadow beneath each tile.
  • Save style.css and check your browser.
  • Each element tile should now have a frosted glass appearance with a faint white border and soft shadow.

Tiles still look flat?

Make sure you placed the new properties inside the existing .element rule block (between the curly braces), not after it.

Check that backdrop-filter is spelled correctly with a hyphen. A typo here silently fails with no error message.

Help me debug my glassmorphism

Add a hover state with smooth transitions

A hover effect makes the table feel interactive. When you mouse over a tile, it should gently lift and glow brighter. The transition property on the base rule ensures the change animates smoothly instead of snapping.

  • In the same .element rule, find the line cursor: pointer; and add this transition property directly below it:
    transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
  • Now add the hover rule directly below the closing brace of .element:
/* Hover: lift the tile and brighten the glass */
.element:hover {
    transform: scale(1.08);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
    background: rgba(255, 255, 255, 0.15);
    z-index: 10;
}

What does this hover state do?

transform: scale(1.08) makes the tile grow to 108% of its size, creating a pop-out effect.

The deeper box-shadow and brighter background make the tile appear closer to the viewer. z-index: 10 ensures the hovered tile overlaps its neighbors instead of sliding behind them.

  • Save style.css and hover your mouse over any element tile in the browser.
  • The tile should smoothly grow larger and glow brighter, then shrink back when you move away.

Hover feels instant instead of smooth?

The transition property must be on the base .element rule, not inside .element:hover. The base rule defines how properties animate. The hover rule only defines the end state.

Why isn't my hover transition animating?

Add decorative background shapes for the blur effect

Right now the blur effect is subtle because there is only a smooth gradient behind the tiles. Glassmorphism looks best when there are colorful shapes behind the glass for the blur to refract.

  • In style.css, find the closing brace of the body rule and add these two pseudo-element rules directly below it:
/* Decorative shapes that give the blur something to refract */
body::before {
    content: '';
    position: fixed;
    top: -150px;
    left: -150px;
    width: 400px;
    height: 400px;
    background: radial-gradient(circle, rgba(106, 17, 203, 0.4), transparent 70%);
    border-radius: 50%;
    z-index: 0;
}

body::after {
    content: '';
    position: fixed;
    bottom: -100px;
    right: -100px;
    width: 350px;
    height: 350px;
    background: radial-gradient(circle, rgba(0, 182, 188, 0.3), transparent 70%);
    border-radius: 50%;
    z-index: 0;
}

What are these pseudo-elements doing?

CSS ::before and ::after create invisible elements attached to the body. Each one is a large circle filled with a soft radial gradient (purple in the top-left corner, teal in the bottom-right).

They sit at z-index: 0 behind the table content (which has z-index: 1). When the element tiles blur whatever is behind them, these colored shapes create a richer, more visible frosted effect.

  • Save style.css and refresh your browser.
  • You should see a faint purple glow in the top-left corner and a teal glow in the bottom-right. The glassmorphism effect on the tiles should now look more pronounced, especially on tiles near those colored areas.

Don't see the colored shapes?

Check that position: relative; is still set on the body rule. Pseudo-elements need a positioned parent to anchor to.

Make sure content: ''; is present on both rules. Without this property, pseudo-elements do not render at all.

Help me fix my pseudo-elements

✔️ Awesome, I've got everything!

Great. Double check you have saved style.css before moving on.

ⓧ I'd like to double check the full code

Here is the complete style.css as it should look at this point:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    min-height: 100vh;
    background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    padding: 20px;
    overflow-x: auto;
    position: relative;
}

body::before {
    content: '';
    position: fixed;
    top: -150px;
    left: -150px;
    width: 400px;
    height: 400px;
    background: radial-gradient(circle, rgba(106, 17, 203, 0.4), transparent 70%);
    border-radius: 50%;
    z-index: 0;
}

body::after {
    content: '';
    position: fixed;
    bottom: -100px;
    right: -100px;
    width: 350px;
    height: 350px;
    background: radial-gradient(circle, rgba(0, 182, 188, 0.3), transparent 70%);
    border-radius: 50%;
    z-index: 0;
}

h1 {
    text-align: center;
    color: #ffffff;
    margin-bottom: 20px;
    font-size: 2rem;
    text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
    position: relative;
    z-index: 1;
}

.table-container {
    overflow-x: auto;
    padding-bottom: 20px;
    position: relative;
    z-index: 1;
}

.periodic-table {
    display: grid;
    grid-template-columns: repeat(18, 100px);
    grid-template-rows: repeat(10, 120px);
    gap: 4px;
    width: fit-content;
    margin: 0 auto;
}

.element {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 6px 4px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.08);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.15);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
    cursor: pointer;
    transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
    position: relative;
}

.element:hover {
    transform: scale(1.08);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
    background: rgba(255, 255, 255, 0.15);
    z-index: 10;
}

.element .atomic-number {
    font-size: 10px;
    color: rgba(255, 255, 255, 0.7);
    align-self: flex-start;
    margin-left: 4px;
}

.element .symbol {
    font-size: 22px;
    font-weight: bold;
    color: #ffffff;
    line-height: 1.2;
}

.element .name {
    font-size: 8px;
    color: rgba(255, 255, 255, 0.85);
    text-align: center;
    line-height: 1.2;
}

.element .mass {
    font-size: 8px;
    color: rgba(255, 255, 255, 0.6);
}

.element .electron-config {
    font-size: 7px;
    color: rgba(255, 255, 255, 0.55);
    text-align: center;
    margin-top: 2px;
    word-break: break-all;
}

Your periodic table now looks like a collection of frosted glass cards floating over a colorful background. Next up, you will color-code each tile by element category so viewers can instantly distinguish metals from gases.

Color-Code by Element Category

Your periodic table is structurally complete with 118 frosted-glass tiles in the correct grid positions. Every tile looks the same though, which makes it hard to distinguish alkali metals from noble gases at a glance.

The JavaScript mapping is already in place. Your getCategoryClass() function assigns a CSS class like alkali-metal or noble-gas to every element div. In this step, you'll write the CSS rules that bring those classes to life with translucent tinted colors, then build a legend so viewers can decode the color scheme.

In this step, get ready to:
  • Add CSS rules that tint each element category with a unique translucent color.
  • Build a glassmorphism-styled legend below the table identifying each category color.
Add category color styles

Each element div already has a category class applied (check the browser DevTools and you'll see classes like element transition-metal). Those classes have no CSS rules yet, so every tile renders with the same default glass background.

  • In style.css, scroll to the bottom of the file (after the .element .electron-config rule).
  • Add the following category color rules:
/* Category-specific tinted glass backgrounds */
.element.diatomic-nonmetal {
    background: rgba(0, 182, 188, 0.15);
    border-color: rgba(0, 182, 188, 0.4);
}

.element.noble-gas {
    background: rgba(206, 7, 173, 0.15);
    border-color: rgba(206, 7, 173, 0.4);
}

.element.alkali-metal {
    background: rgba(255, 80, 80, 0.15);
    border-color: rgba(255, 80, 80, 0.4);
}

.element.alkaline-earth-metal {
    background: rgba(255, 170, 0, 0.15);
    border-color: rgba(255, 170, 0, 0.4);
}

.element.metalloid {
    background: rgba(255, 165, 0, 0.15);
    border-color: rgba(255, 165, 0, 0.4);
}

.element.polyatomic-nonmetal {
    background: rgba(0, 150, 160, 0.15);
    border-color: rgba(0, 150, 160, 0.4);
}

.element.post-transition-metal {
    background: rgba(245, 57, 81, 0.15);
    border-color: rgba(245, 57, 81, 0.4);
}

.element.transition-metal {
    background: rgba(79, 187, 98, 0.15);
    border-color: rgba(79, 187, 98, 0.4);
}

.element.lanthanide {
    background: rgba(100, 149, 237, 0.15);
    border-color: rgba(100, 149, 237, 0.4);
}

.element.actinide {
    background: rgba(150, 111, 214, 0.15);
    border-color: rgba(150, 111, 214, 0.4);
}

.element.unknown {
    background: rgba(180, 180, 180, 0.1);
    border-color: rgba(180, 180, 180, 0.3);
}

What does this code do?

Each rule targets a compound class selector like .element.alkali-metal. This means the rule only applies to a div that has both the element class AND the category class.

The low alpha values (0.15 for background, 0.4 for border) keep the glass effect intact. You can still see the blurred background through each tile, but now with a colored tint layered on top.

  • Save style.css.
  • Refresh your browser. Each element tile should now show a distinct translucent color tint based on its category. Alkali metals (leftmost column) appear reddish, noble gases (rightmost column) appear pinkish-purple, and transition metals (the large middle block) appear green-tinted.

All tiles still look the same?

Open DevTools (F12) and inspect one tile. Look for a class like element alkali-metal. If the category class is missing, double-check that getCategoryClass() is being called inside createElement() in script.js.

If the class IS present but no color shows, make sure there is no space between .element and the category class in your CSS selector. .element.alkali-metal (no space) targets one element with both classes. .element .alkali-metal (with space) targets a child element, which is wrong.

Why aren't my element tiles showing category colors?

Build the legend data and rendering logic

With 11 possible category colors on screen, a viewer needs a reference to decode them. You'll create a legend array that pairs each category name with its display color, then loop through it to build colored swatches.

  • Switch back to script.js.
  • Find the line that says function getCategoryClass(category) {.
  • Add the following LEGEND_CATEGORIES array directly above that function:
// Legend display data: name, CSS class, and swatch color for each category
const LEGEND_CATEGORIES = [
    { name: "Alkali Metal", class: "alkali-metal", color: "rgba(255, 80, 80, 0.5)" },
    { name: "Alkaline Earth Metal", class: "alkaline-earth-metal", color: "rgba(255, 170, 0, 0.5)" },
    { name: "Transition Metal", class: "transition-metal", color: "rgba(79, 187, 98, 0.5)" },
    { name: "Post-transition Metal", class: "post-transition-metal", color: "rgba(245, 57, 81, 0.5)" },
    { name: "Metalloid", class: "metalloid", color: "rgba(255, 165, 0, 0.5)" },
    { name: "Nonmetal", class: "diatomic-nonmetal", color: "rgba(0, 182, 188, 0.5)" },
    { name: "Noble Gas", class: "noble-gas", color: "rgba(206, 7, 173, 0.5)" },
    { name: "Lanthanide", class: "lanthanide", color: "rgba(100, 149, 237, 0.5)" },
    { name: "Actinide", class: "actinide", color: "rgba(150, 111, 214, 0.5)" },
    { name: "Unknown", class: "unknown", color: "rgba(180, 180, 180, 0.4)" }
];

What does this code do?

This array stores the display information for the legend. Each entry has a human-readable name, the matching CSS class, and a color value at a higher opacity (0.5) than the tile backgrounds (0.15) so the swatch is easier to see at small sizes.

  • Now find the createLegend() function further down in the file.
  • Replace the entire function with this updated version:
function createLegend() {
    const legend = document.getElementById("legend");

    // Loop through each category and create a colored swatch + label
    LEGEND_CATEGORIES.forEach(cat => {
        const item = document.createElement("div");
        item.className = "legend-item";

        const color = document.createElement("div");
        color.className = "legend-color";
        color.style.backgroundColor = cat.color;

        const label = document.createElement("span");
        label.textContent = cat.name;

        item.appendChild(color);
        item.appendChild(label);
        legend.appendChild(item);
    });
}

What does this code do?

The function loops through LEGEND_CATEGORIES and for each entry creates a small colored square (legend-color) paired with the category name (legend-item).

Each swatch's background color is set inline from the array's color field. The init() function already calls createLegend() after rendering the elements, so the legend appears automatically.

  • Save script.js.
  • Refresh your browser. You should see colored text labels appearing below the periodic table, though they won't be styled nicely yet.

Legend not appearing at all?

Open the browser console (F12, then **Console** tab). If you see an error about LEGEND_CATEGORIES is not defined, make sure the array is declared above the createLegend() function, not inside it.

My legend isn't rendering below the periodic table. Help me debug.

Style the legend container

The legend content is rendering, but it needs the same glassmorphism treatment as the rest of the page to look cohesive.

  • Switch back to style.css.
  • Add the following legend styles at the bottom of the file (after the category class rules you just added):
/* Legend container with glassmorphism */
.legend {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
    margin-top: 20px;
    padding: 16px;
    background: rgba(255, 255, 255, 0.05);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-radius: 12px;
    border: 1px solid rgba(255, 255, 255, 0.1);
    max-width: 1200px;
    margin-left: auto;
    margin-right: auto;
    position: relative;
    z-index: 1;
}

/* Each legend entry: color swatch + label side by side */
.legend-item {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    color: rgba(255, 255, 255, 0.8);
}

/* Small colored square representing the category */
.legend-color {
    width: 14px;
    height: 14px;
    border-radius: 4px;
}

What does this code do?

The .legend container uses flex-wrap: wrap so category items flow onto multiple lines if they don't fit in one row. The backdrop-filter: blur(8px) gives it the same frosted-glass look as the element tiles.

Each .legend-item pairs a 14x14px color swatch with a text label. The margin-left: auto and margin-right: auto center the legend below the table.

  • Save style.css and refresh your browser.
  • You should see a frosted-glass legend bar centered below the periodic table. It displays 10 colored swatches with labels like "Alkali Metal", "Transition Metal", and "Noble Gas".

Legend looks cramped or misaligned?

If the swatches and labels overlap, confirm that .legend-item has gap: 6px and that .legend has gap: 12px. The inner gap spaces the swatch from the label. The outer gap spaces each legend entry from its neighbors.

My legend layout looks broken. Help me fix the spacing.

✔️ Awesome, I've got everything!

Great. Double check you've saved both style.css and script.js.

ⓧ I'd like to double check the full code

Here is the complete style.css at this point:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    min-height: 100vh;
    background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    padding: 20px;
    overflow-x: auto;
    position: relative;
}

body::before {
    content: '';
    position: fixed;
    top: -150px;
    left: -150px;
    width: 400px;
    height: 400px;
    background: radial-gradient(circle, rgba(106, 17, 203, 0.4), transparent 70%);
    border-radius: 50%;
    z-index: 0;
}

body::after {
    content: '';
    position: fixed;
    bottom: -100px;
    right: -100px;
    width: 350px;
    height: 350px;
    background: radial-gradient(circle, rgba(0, 182, 188, 0.3), transparent 70%);
    border-radius: 50%;
    z-index: 0;
}

h1 {
    text-align: center;
    color: #ffffff;
    margin-bottom: 20px;
    font-size: 2rem;
    text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
    position: relative;
    z-index: 1;
}

.table-container {
    overflow-x: auto;
    padding-bottom: 20px;
    position: relative;
    z-index: 1;
}

.periodic-table {
    display: grid;
    grid-template-columns: repeat(18, 100px);
    grid-template-rows: repeat(10, 120px);
    gap: 4px;
    width: fit-content;
    margin: 0 auto;
}

.element {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 6px 4px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.08);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.15);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
    cursor: pointer;
    transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
    position: relative;
}

.element:hover {
    transform: scale(1.08);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
    background: rgba(255, 255, 255, 0.15);
    z-index: 10;
}

.element .atomic-number {
    font-size: 10px;
    color: rgba(255, 255, 255, 0.7);
    align-self: flex-start;
    margin-left: 4px;
}

.element .symbol {
    font-size: 22px;
    font-weight: bold;
    color: #ffffff;
    line-height: 1.2;
}

.element .name {
    font-size: 8px;
    color: rgba(255, 255, 255, 0.85);
    text-align: center;
    line-height: 1.2;
}

.element .mass {
    font-size: 8px;
    color: rgba(255, 255, 255, 0.6);
}

.element .electron-config {
    font-size: 7px;
    color: rgba(255, 255, 255, 0.55);
    text-align: center;
    margin-top: 2px;
    word-break: break-all;
}

.element.diatomic-nonmetal {
    background: rgba(0, 182, 188, 0.15);
    border-color: rgba(0, 182, 188, 0.4);
}

.element.noble-gas {
    background: rgba(206, 7, 173, 0.15);
    border-color: rgba(206, 7, 173, 0.4);
}

.element.alkali-metal {
    background: rgba(255, 80, 80, 0.15);
    border-color: rgba(255, 80, 80, 0.4);
}

.element.alkaline-earth-metal {
    background: rgba(255, 170, 0, 0.15);
    border-color: rgba(255, 170, 0, 0.4);
}

.element.metalloid {
    background: rgba(255, 165, 0, 0.15);
    border-color: rgba(255, 165, 0, 0.4);
}

.element.polyatomic-nonmetal {
    background: rgba(0, 150, 160, 0.15);
    border-color: rgba(0, 150, 160, 0.4);
}

.element.post-transition-metal {
    background: rgba(245, 57, 81, 0.15);
    border-color: rgba(245, 57, 81, 0.4);
}

.element.transition-metal {
    background: rgba(79, 187, 98, 0.15);
    border-color: rgba(79, 187, 98, 0.4);
}

.element.lanthanide {
    background: rgba(100, 149, 237, 0.15);
    border-color: rgba(100, 149, 237, 0.4);
}

.element.actinide {
    background: rgba(150, 111, 214, 0.15);
    border-color: rgba(150, 111, 214, 0.4);
}

.element.unknown {
    background: rgba(180, 180, 180, 0.1);
    border-color: rgba(180, 180, 180, 0.3);
}

.legend {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
    margin-top: 20px;
    padding: 16px;
    background: rgba(255, 255, 255, 0.05);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border-radius: 12px;
    border: 1px solid rgba(255, 255, 255, 0.1);
    max-width: 1200px;
    margin-left: auto;
    margin-right: auto;
    position: relative;
    z-index: 1;
}

.legend-item {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    color: rgba(255, 255, 255, 0.8);
}

.legend-color {
    width: 14px;
    height: 14px;
    border-radius: 4px;
}

And here is the complete script.js at this point:

const DATA_URL = "https://raw.githubusercontent.com/Bowserinator/Periodic-Table-JSON/master/PeriodicTableJSON.json";

const CATEGORY_CLASSES = {
    "diatomic nonmetal": "diatomic-nonmetal",
    "noble gas": "noble-gas",
    "alkali metal": "alkali-metal",
    "alkaline earth metal": "alkaline-earth-metal",
    "metalloid": "metalloid",
    "polyatomic nonmetal": "polyatomic-nonmetal",
    "post-transition metal": "post-transition-metal",
    "transition metal": "transition-metal",
    "lanthanide": "lanthanide",
    "actinide": "actinide"
};

const LEGEND_CATEGORIES = [
    { name: "Alkali Metal", class: "alkali-metal", color: "rgba(255, 80, 80, 0.5)" },
    { name: "Alkaline Earth Metal", class: "alkaline-earth-metal", color: "rgba(255, 170, 0, 0.5)" },
    { name: "Transition Metal", class: "transition-metal", color: "rgba(79, 187, 98, 0.5)" },
    { name: "Post-transition Metal", class: "post-transition-metal", color: "rgba(245, 57, 81, 0.5)" },
    { name: "Metalloid", class: "metalloid", color: "rgba(255, 165, 0, 0.5)" },
    { name: "Nonmetal", class: "diatomic-nonmetal", color: "rgba(0, 182, 188, 0.5)" },
    { name: "Noble Gas", class: "noble-gas", color: "rgba(206, 7, 173, 0.5)" },
    { name: "Lanthanide", class: "lanthanide", color: "rgba(100, 149, 237, 0.5)" },
    { name: "Actinide", class: "actinide", color: "rgba(150, 111, 214, 0.5)" },
    { name: "Unknown", class: "unknown", color: "rgba(180, 180, 180, 0.4)" }
];

function getCategoryClass(category) {
    if (!category) return "unknown";
    for (const [key, value] of Object.entries(CATEGORY_CLASSES)) {
        if (category.includes(key)) return value;
    }
    return "unknown";
}

function createElement(element) {
    const div = document.createElement("div");
    const categoryClass = getCategoryClass(element.category);
    div.className = `element ${categoryClass}`;
    div.style.gridColumn = element.xpos;
    div.style.gridRow = element.ypos;

    div.innerHTML = `
        <span class="atomic-number">${element.number}</span>
        <span class="symbol">${element.symbol}</span>
        <span class="name">${element.name}</span>
        <span class="mass">${element.atomic_mass.toFixed(2)}</span>
        <span class="electron-config">${element.electron_configuration_semantic}</span>
    `;

    return div;
}

function createLegend() {
    const legend = document.getElementById("legend");

    LEGEND_CATEGORIES.forEach(cat => {
        const item = document.createElement("div");
        item.className = "legend-item";

        const color = document.createElement("div");
        color.className = "legend-color";
        color.style.backgroundColor = cat.color;

        const label = document.createElement("span");
        label.textContent = cat.name;

        item.appendChild(color);
        item.appendChild(label);
        legend.appendChild(item);
    });
}

async function init() {
    try {
        const response = await fetch(DATA_URL);
        const data = await response.json();
        const table = document.getElementById("periodic-table");

        data.elements.forEach(element => {
            const el = createElement(element);
            table.appendChild(el);
        });

        createLegend();
    } catch (error) {
        console.error("Failed to load periodic table data:", error);
        document.getElementById("periodic-table").innerHTML =
            '<p style="color: white; grid-column: 1 / -1;">Failed to load element data. Please check your internet connection.</p>';
    }
}

init();

Why does getCategoryClass use includes() instead of strict equality?

Some elements in the JSON have category values like "unknown, probably transition metal" or "unknown, predicted to be noble gas". Using includes() catches these partial matches so edge-case elements still get a color rather than falling into the generic "unknown" bucket.

Your periodic table now communicates structure at a glance. The color coding turns a wall of identical glass tiles into a meaningful visualization where viewers can instantly spot element families. Next up, you'll take on a Secret Mission to add click-to-expand detail modals with layered glassmorphism styling.

Secret mission

Add an Element Detail Modal

What if clicking an element revealed more? Add a glassmorphism-styled modal that shows expanded information like phase, density, discovery history, and a summary for each element. You will layer glass on glass for a polished interactive experience.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.

Resources you used:

  • The periodic-table project folder on your Desktop (contains index.html, style.css, and script.js).
  • The Live Server extension installed in VS Code.

Keep everything running

No action needed. Choose this if you're still actively building or want to experiment with the periodic table further.

  • Your periodic-table folder stays on your Desktop, ready to open anytime.
  • The Live Server extension remains installed in VS Code for future projects.

Pause - I'll come back to this later

Stop Live Server to free up the local port it uses. Your files stay intact for when you return.

  • In VS Code, click Port: 5500 in the bottom status bar to stop Live Server.
  • Close the browser tab showing your periodic table.

Delete - I don't want to use this again

Remove all project files and the Live Server extension to start fresh.

  • Stop Live Server by clicking Port: 5500 in the VS Code status bar.
  • Delete the project folder by running this command in your terminal (macOS/Linux):
rm -rf ~/Desktop/periodic-table

On Windows, run this in PowerShell instead:

Remove-Item -Recurse -Force ~\Desktop\periodic-table
  • To uninstall the Live Server extension, open the Extensions panel in VS Code (Cmd+Shift+X on macOS or Ctrl+Shift+X on Windows).
  • Search for Live Server, click on it, then click Uninstall.

Nice Work!

Nice Work!

You just built a complete glassmorphism periodic table from scratch. All 118 elements are rendered dynamically from live data, positioned in an accurate 18-column grid layout, and styled with frosted-glass effects.

Here's what you learned:

  • Fetched and rendered all 118 elements from an external JSON source using the JavaScript fetch API, displaying five data points per tile.
  • Implemented CSS Grid with an 18-column layout that correctly positions every element including lanthanides and actinides.
  • Applied glassmorphism design using backdrop-filter, semi-transparent backgrounds, and color-coded category tints with a visual legend.
  • Secret Mission: Created a click-to-expand detail modal with additional element information, styled with layered glassmorphism.

Ready to quiz yourself?