Interactive Periodic Table
Abstract
The periodic table is a beautiful data-visualization problem: 118 elements, each with a dozen properties, arranged in a grid that itself encodes meaning (rows = periods, columns = groups). In this tutorial you build a Tkinter app where each element is a clickable button that pops up its details. Then you scale it from 10 elements to all 118, lay them out in the real periodic-table shape, color them by category (metals, nonmetals, noble gases), and add live search and filtering.
You will leave understanding:
- How to generate a grid of widgets from a data list with
grid(row=, column=)grid(row=, column=). - The classic late-binding lambda bug in loops — and the
e=elemente=elementfix. - How to drive layout from data (atomic number → grid position).
- How to color-code and filter a dataset in a GUI.
Prerequisites
- Python 3.6 or above.
- A text editor or IDE.
- Tkinter (bundled with Python).
- Comfort with lists, dictionaries, and loops.
Getting Started
Create the project
- Create a folder named
periodic-tableperiodic-table. - Inside it, create
interactive_periodic_table.pyinteractive_periodic_table.py.
Write the code
interactive_periodic_table.py
Source"""
Interactive Periodic Table
A Python application that displays an interactive periodic table. Features include:
- Displaying information about elements when clicked.
- Searching for elements by name or symbol.
"""
import tkinter as tk
from tkinter import messagebox
# Sample data for the periodic table
elements = [
{"symbol": "H", "name": "Hydrogen", "atomic_number": 1, "atomic_mass": 1.008},
{"symbol": "He", "name": "Helium", "atomic_number": 2, "atomic_mass": 4.0026},
{"symbol": "Li", "name": "Lithium", "atomic_number": 3, "atomic_mass": 6.94},
{"symbol": "Be", "name": "Beryllium", "atomic_number": 4, "atomic_mass": 9.0122},
{"symbol": "B", "name": "Boron", "atomic_number": 5, "atomic_mass": 10.81},
{"symbol": "C", "name": "Carbon", "atomic_number": 6, "atomic_mass": 12.011},
{"symbol": "N", "name": "Nitrogen", "atomic_number": 7, "atomic_mass": 14.007},
{"symbol": "O", "name": "Oxygen", "atomic_number": 8, "atomic_mass": 15.999},
{"symbol": "F", "name": "Fluorine", "atomic_number": 9, "atomic_mass": 18.998},
{"symbol": "Ne", "name": "Neon", "atomic_number": 10, "atomic_mass": 20.180},
]
class InteractivePeriodicTable:
def __init__(self, root):
self.root = root
self.root.title("Interactive Periodic Table")
self.create_table()
def create_table(self):
"""Create the periodic table layout."""
for i, element in enumerate(elements):
button = tk.Button(
self.root,
text=element["symbol"],
width=10,
height=3,
command=lambda e=element: self.show_element_info(e),
)
button.grid(row=i // 5, column=i % 5, padx=5, pady=5)
def show_element_info(self, element):
"""Display information about the selected element."""
info = (
f"Name: {element['name']}\n"
f"Symbol: {element['symbol']}\n"
f"Atomic Number: {element['atomic_number']}\n"
f"Atomic Mass: {element['atomic_mass']}"
)
messagebox.showinfo("Element Information", info)
def main():
root = tk.Tk()
app = InteractivePeriodicTable(root)
root.mainloop()
if __name__ == "__main__":
main()
"""
Interactive Periodic Table
A Python application that displays an interactive periodic table. Features include:
- Displaying information about elements when clicked.
- Searching for elements by name or symbol.
"""
import tkinter as tk
from tkinter import messagebox
# Sample data for the periodic table
elements = [
{"symbol": "H", "name": "Hydrogen", "atomic_number": 1, "atomic_mass": 1.008},
{"symbol": "He", "name": "Helium", "atomic_number": 2, "atomic_mass": 4.0026},
{"symbol": "Li", "name": "Lithium", "atomic_number": 3, "atomic_mass": 6.94},
{"symbol": "Be", "name": "Beryllium", "atomic_number": 4, "atomic_mass": 9.0122},
{"symbol": "B", "name": "Boron", "atomic_number": 5, "atomic_mass": 10.81},
{"symbol": "C", "name": "Carbon", "atomic_number": 6, "atomic_mass": 12.011},
{"symbol": "N", "name": "Nitrogen", "atomic_number": 7, "atomic_mass": 14.007},
{"symbol": "O", "name": "Oxygen", "atomic_number": 8, "atomic_mass": 15.999},
{"symbol": "F", "name": "Fluorine", "atomic_number": 9, "atomic_mass": 18.998},
{"symbol": "Ne", "name": "Neon", "atomic_number": 10, "atomic_mass": 20.180},
]
class InteractivePeriodicTable:
def __init__(self, root):
self.root = root
self.root.title("Interactive Periodic Table")
self.create_table()
def create_table(self):
"""Create the periodic table layout."""
for i, element in enumerate(elements):
button = tk.Button(
self.root,
text=element["symbol"],
width=10,
height=3,
command=lambda e=element: self.show_element_info(e),
)
button.grid(row=i // 5, column=i % 5, padx=5, pady=5)
def show_element_info(self, element):
"""Display information about the selected element."""
info = (
f"Name: {element['name']}\n"
f"Symbol: {element['symbol']}\n"
f"Atomic Number: {element['atomic_number']}\n"
f"Atomic Mass: {element['atomic_mass']}"
)
messagebox.showinfo("Element Information", info)
def main():
root = tk.Tk()
app = InteractivePeriodicTable(root)
root.mainloop()
if __name__ == "__main__":
main()
Run it
C:\Users\Your Name\periodic-table> python interactive_periodic_table.py
# A grid of element buttons appears. Click one to see its details.C:\Users\Your Name\periodic-table> python interactive_periodic_table.py
# A grid of element buttons appears. Click one to see its details.Step-by-Step Explanation
1. Elements as data
elements = [
{"symbol": "H", "name": "Hydrogen", "atomic_number": 1, "atomic_mass": 1.008},
{"symbol": "He", "name": "Helium", "atomic_number": 2, "atomic_mass": 4.0026},
...
]elements = [
{"symbol": "H", "name": "Hydrogen", "atomic_number": 1, "atomic_mass": 1.008},
{"symbol": "He", "name": "Helium", "atomic_number": 2, "atomic_mass": 4.0026},
...
]Each element is a dict. The UI is generated from this list — add an element here and a button appears automatically. This data-drives-the-view idea is the heart of the project.
2. Building the grid
for i, element in enumerate(elements):
button = tk.Button(
self.root, text=element["symbol"], width=10, height=3,
command=lambda e=element: self.show_element_info(e),
)
button.grid(row=i // 5, column=i % 5, padx=5, pady=5)for i, element in enumerate(elements):
button = tk.Button(
self.root, text=element["symbol"], width=10, height=3,
command=lambda e=element: self.show_element_info(e),
)
button.grid(row=i // 5, column=i % 5, padx=5, pady=5)gridgrid places widgets in rows and columns. The integer math is the trick: i // 5i // 5 is the row, i % 5i % 5 is the column — so elements flow left-to-right, wrapping every 5. Change the 55 to change the columns per row.
3. The lambda gotcha (critical)
command=lambda e=element: self.show_element_info(e)command=lambda e=element: self.show_element_info(e)Why e=elemente=element and not just lambda: self.show_element_info(element)lambda: self.show_element_info(element)? Because of late binding: a bare lambda captures the variable elementelement, not its current value. By the time you click, the loop has finished and elementelement points at the last item — so every button would show Neon. Binding e=elemente=element as a default argument snapshots the value now. This is the single most common Tkinter-loop bug.
4. Showing details
def show_element_info(self, element):
info = (
f"Name: {element['name']}\n"
f"Symbol: {element['symbol']}\n"
f"Atomic Number: {element['atomic_number']}\n"
f"Atomic Mass: {element['atomic_mass']}"
)
messagebox.showinfo("Element Information", info)def show_element_info(self, element):
info = (
f"Name: {element['name']}\n"
f"Symbol: {element['symbol']}\n"
f"Atomic Number: {element['atomic_number']}\n"
f"Atomic Mass: {element['atomic_mass']}"
)
messagebox.showinfo("Element Information", info)A popup is fine to start; a side panel that updates in place feels far more like a real app (below).
Scale to All 118 Elements — in the Real Layout
The actual periodic table isn’t a 5-wide wrap; it’s an 18-column grid with gaps. Store each element’s groupgroup (column) and periodperiod (row) and place it precisely:
# Each element gains "period" and "group" fields
for el in elements:
tk.Button(self.root, text=el["symbol"], width=4, height=2,
command=lambda e=el: self.show(e)).grid(
row=el["period"], column=el["group"], padx=1, pady=1)# Each element gains "period" and "group" fields
for el in elements:
tk.Button(self.root, text=el["symbol"], width=4, height=2,
command=lambda e=el: self.show(e)).grid(
row=el["period"], column=el["group"], padx=1, pady=1)The lanthanides and actinides sit in two detached rows below — exactly as on a wall chart. Pull a full dataset from a JSON file rather than typing 118 dicts by hand.
Color by Category
Chemistry uses color to group elements. Map categories to colors:
CATEGORY_COLORS = {
"alkali metal": "#ff6666", "noble gas": "#66ccff",
"nonmetal": "#99ff99", "metalloid": "#ffcc66", "transition metal": "#cccccc",
}
btn.config(bg=CATEGORY_COLORS.get(el["category"], "white"))CATEGORY_COLORS = {
"alkali metal": "#ff6666", "noble gas": "#66ccff",
"nonmetal": "#99ff99", "metalloid": "#ffcc66", "transition metal": "#cccccc",
}
btn.config(bg=CATEGORY_COLORS.get(el["category"], "white"))Suddenly the table teaches — trends jump out visually.
Add Search and Filtering
An Entry that highlights matches as you type:
def on_search(self, query):
q = query.lower()
for el, btn in self.buttons.items():
match = q in el["name"].lower() or q == el["symbol"].lower()
btn.config(relief="solid" if match and q else "raised")def on_search(self, query):
q = query.lower()
for el, btn in self.buttons.items():
match = q in el["name"].lower() or q == el["symbol"].lower()
btn.config(relief="solid" if match and q else "raised")Filter by category with a dropdown to dim everything except, say, the noble gases.
Upgrade the Detail View
Replace the popup with a persistent side panel showing electron configuration, melting/boiling points, discovery year, and a short description. Update it on click instead of spawning dialogs — it feels like a reference tool, not a quiz.
Common Mistakes
| Problem | Cause | Fix |
|---|---|---|
| Every button shows the last element | Late-binding lambda | Bind the value: lambda e=element: ...lambda e=element: ... |
| Elements pile in one column | Wrong rowrow/columncolumn math | Use i // colsi // cols and i % colsi % cols, or real group/period |
| Layout looks nothing like the chart | Used a simple wrap | Place by groupgroup/periodperiod with gaps |
| Window huge or buttons tiny | Fixed sizes vs. 118 cells | Smaller widthwidth/heightheight; let the grid expand |
| Typing 118 dicts by hand is error-prone | Hard-coded data | Load elements from a JSON dataset |
| Search does nothing | Compared against symbol case-sensitively | Normalize with .lower().lower() |
Variations to Try
- Full dataset — all 118 elements with 15+ properties from JSON.
- Trend heatmaps — color by electronegativity or atomic radius.
- Element quiz — “click the noble gases” timed challenge.
- Compare mode — select two elements and diff their properties.
- 3D Bohr model — draw electron shells for the selected element.
- Unit toggles — switch mass/temperature units.
- Export — save the selected element as a flashcard image.
Real-World Applications
- Education — chemistry teaching and revision tools.
- Reference apps — quick element lookup for students and engineers.
- Data-viz practice — encoding multiple dimensions (color, position) at once.
- Material selection — filtering elements by property ranges.
Educational Value
- Data-driven UI — generating widgets from a dataset.
- Grid layout math — mapping an index (or group/period) to position.
- Closures & late binding — the most-asked-about Python loop bug.
- Visual encoding — color and position carrying meaning.
Next Steps
- Load all 118 elements from a JSON dataset.
- Lay them out in the real group/period grid.
- Color by category and add search + filtering.
- Swap the popup for a live detail panel.
Conclusion
You built an interactive periodic table, dodged the infamous late-binding lambda trap, and learned to drive a UI entirely from data. Scaled to 118 color-coded, searchable elements in their true layout, it becomes a genuine reference and teaching tool — and the data-drives-the-view pattern you practiced powers dashboards, seating charts, and any grid-of-things UI you’ll build next. Full source on GitHub. Explore more visualization projects on Python Central Hub.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
