How I Built a 3D Atom Site with an Interactive Table
Ever wanted to build your own interactive 3D website? Follow my journey of creating a 3D atom viewer and periodic table using Three.js and React.
Alex Porter
Creative developer and 3D enthusiast passionate about building beautiful, interactive web experiences.
From 2D to 3D: How I Built an Interactive Atom Viewer with Three.js and React
Remember those static, flat diagrams of atoms in your high school chemistry book? A circle for the nucleus, a few other circles for electrons, and some concentric rings. They get the point across, but they always felt a little... lifeless. I’ve always been fascinated by the intersection of science and digital art, and I wondered: could I make something better? Something interactive, intuitive, and just plain cool?
That question sparked a weekend project that quickly became an obsession: building a web application that combines a fully interactive periodic table with a dynamic, 3D visualization of any selected atom. It was a journey filled with challenges, learning, and a whole lot of console.log()
. Today, I want to walk you through how I built it, from the initial idea to the final deployed site.
The Spark: Why Build a 3D Atom Viewer?
My goal was simple: create an educational tool that felt more like a toy. I wanted a user to be able to click on an element, say Helium, and instantly see a 3D model of it, complete with two protons in the nucleus and two electrons zipping around. Click on Gold, and see its 79 electrons orbiting in their distinct shells. This project was the perfect way to combine my love for React with a desire to finally dive deep into Three.js, the leading library for 3D graphics on the web.
Choosing the Right Tools: The Tech Stack
Every project starts with a few key decisions. For this, the technology choices were critical. I needed a stack that was powerful, flexible, and had a strong community.
Technology | Role | Why I Chose It |
---|---|---|
React | UI Framework | Its component-based architecture is perfect for splitting the app into manageable pieces like the PeriodicTable and the AtomCanvas . |
Three.js | 3D Graphics | The de-facto standard for WebGL. It has amazing documentation and simplifies the complexities of 3D rendering in the browser. |
React Three Fiber | React Renderer for Three.js | This is the secret sauce. It lets you write Three.js scenes declaratively with reusable React components, which is an absolute game-changer. |
Zustand | State Management | Lightweight, simple, and powerful. It avoids boilerplate and makes sharing state (like the currently selected element) between components a breeze. |
Tailwind CSS | Styling | For rapidly building and styling the 2D interface (the periodic table) without writing custom CSS files. |
Building the Foundation: Setting Up the React App
I kicked things off with Vite, my go-to build tool for modern web projects. It’s incredibly fast.
npm create vite@latest 3d-atom-viewer -- --template react-ts
After scaffolding the project, I installed the core dependencies:
npm install three @react-three/fiber @react-three/drei zustand tailwindcss
My initial component structure looked something like this:
App.jsx
: The main container, holding the layout.components/AtomCanvas.jsx
: The component responsible for the entire 3D scene.components/AtomModel.jsx
: A component that would actually build and render the selected atom.components/PeriodicTable.jsx
I: The grid of elements.data/elements.json
: A JSON file containing all the data for the 118 elements.
Creating the 3D Scene with Three.js
Thanks to React Three Fiber (R3F), setting up a 3D scene inside a React component is beautifully simple. Instead of the typical imperative Three.js code, you write JSX.
My AtomCanvas.jsx
component looked like this initially:
import { Canvas } from '@react-three/fiber' import { OrbitControls } from '@react-three/drei' function AtomCanvas() { return ( <Canvas camera={{ position: [0, 0, 50], fov: 45 }}> <ambientLight intensity={0.5} /> <pointLight position={[10, 10, 10]} intensity={1} /> {/* Our AtomModel will go here */} <OrbitControls /> </Canvas> ) }
Let’s break this down:
<Canvas>
: This R3F component sets up the Three.jsScene
andRenderer
for you.camera
: I configured the camera's initial position to be looking at the origin from a distance.<ambientLight>
&<pointLight>
: Lighting is crucial in 3D! An ambient light illuminates everything globally, while a point light acts like a lightbulb, creating shadows and highlights.<OrbitControls>
: This handy helper from thedrei
library lets users rotate, pan, and zoom the camera with their mouse. Instant interactivity!
Modeling the Atom: Nucleus, Shells, and Electrons
This was the most exciting part. How do you represent an atom in 3D? I decided on a simplified Bohr model.
The Nucleus
The nucleus is a cluster of protons and neutrons. I represented it as a single sphere. The color and size could eventually be tied to the element's properties, but for now, a simple sphere works.
function Nucleus() { return ( <mesh> <sphereGeometry args={[2, 32, 32]} /> <meshStandardMaterial color="#FF5733" /> </mesh> ) }
Electrons and Orbits
This was trickier. I needed to: 1. Draw the orbital paths. 2. Place the electrons on those paths. 3. Animate the electrons.
For the orbits, I used a TorusGeometry
. It’s a donut shape, which is perfect for a simple circular orbit. I made it very thin to look like a line.
For the electrons, I created a small sphere. The real challenge was positioning them. For an electron on a circular path, you can use basic trigonometry:
x = radius * cos(angle)
y = radius * sin(angle)
By animating the angle
over time, the electron moves! I used R3F's useFrame
hook, which runs a function on every single rendered frame.
Here's a simplified component for a single animated electron:
import { useRef } from 'react' import { useFrame } from '@react-three/fiber' function Electron({ radius = 5, speed = 1 }) { const ref = useRef() // Animate the electron's position on each frame useFrame((state) => { const t = state.clock.getElapsedTime() * speed ref.current.position.x = radius * Math.cos(t) ref.current.position.y = radius * Math.sin(t) }) return ( <mesh ref={ref}> <sphereGeometry args={[0.5, 16, 16]} /> <meshStandardMaterial color="#3399FF" emissive="#3399FF" emissiveIntensity={2} /> </mesh> ) }
The real magic was combining this. My AtomModel
component would read the electron configuration of the selected element (e.g., Carbon: 2 electrons in the first shell, 4 in the second) and dynamically render the correct number of orbits and electrons, each with a different radius and slightly different speed.
Bringing Data to Life: The Interactive Periodic Table
With the 3D part taking shape, I needed the 2D interface. I created a PeriodicTable.jsx
component that fetched data from my elements.json
file. Using Tailwind CSS, I mapped over the data to render a grid.
// Simplified snippet from PeriodicTable.jsx import elements from '../data/elements.json' function PeriodicTable({ onElementClick }) { return ( <div className="grid grid-cols-18 gap-1"> {elements.map(element => ( <button key={element.name} className="p-2 text-center border rounded ..." style={{ gridColumn: element.xpos, gridRow: element.ypos }} onClick={() => onElementClick(element)} > <strong>{element.symbol}</strong> <span className="text-xs">{element.name}</span> </button> ))} </div> ) }
Each button was styled based on its element category (alkali metal, noble gas, etc.), and its position in the grid was determined by `xpos` and `ypos` from the JSON data. The most important part was the onClick
handler.
Connecting the Pieces: State Management Magic
So, how does clicking a button on the table update the 3D canvas? This is where state management comes in. I used Zustand to create a simple store.
1. Create the store:
// store.js import { create } from 'zustand' import elements from './data/elements.json' export const useElementStore = create((set) => ({ selectedElement: elements.find(el => el.number === 1), // Default to Hydrogen setSelectedElement: (element) => set({ selectedElement: element }), }))
2. Use the store in the components:
In PeriodicTable.jsx
, I'd call the action to update the state:
const setSelectedElement = useElementStore((state) => state.setSelectedElement) // ... in the onClick: onClick={() => setSelectedElement(element)}
In AtomCanvas.jsx
, I'd listen for changes to the state:
const selectedElement = useElementStore((state) => state.selectedElement) // ... then pass this to the model: <AtomModel element={selectedElement} />
Now, whenever `selectedElement` changes, the `AtomModel` component automatically re-renders with the new element's data, rebuilding the nucleus and electron shells. It felt like magic seeing it work for the first time.
Challenges and Lessons Learned
- Performance is Key: Rendering hundreds of electrons for heavier elements can be slow. I had to learn about optimization techniques like instanced meshes. For this project, I capped the animated electrons and represented outer shells more simply to keep things smooth.
- Data Wrangling: Finding and formatting a reliable JSON database of all elements with their correct electron shell configurations was a mini-project in itself. Accuracy was paramount.
- The Beauty of R3F: I can't overstate how much React Three Fiber and Drei simplified this project. Managing a Three.js project in a traditional way would have been ten times more complex. Thinking in components, even for 3D objects, is incredibly powerful.
Final Thoughts and What's Next
Building this 3D atom viewer was an incredibly rewarding experience. It pushed my React skills, forced me to finally learn Three.js, and resulted in a tool that’s both educational and fun to play with. It's a testament to the power of the modern web stack that a project like this is achievable for a solo developer.
What's next? I'd love to add more scientific detail, like showing more accurate orbital shapes (s, p, d, f orbitals) instead of simple circles, visualizing isotopes, and maybe even exploring a VR/AR version. For now, I'm just thrilled to have turned a static textbook diagram into a living, breathing digital model. I hope this inspires you to build your own interactive 3D experiences!