An interactive 3D illustration of the human dentition across an entire lifetime — from bare gums at birth to the changes of old age.
Live: learn-teeth-anatomy.vercel.app — English · Bahasa Indonesia
Drag one age slider and the whole story plays out: baby teeth breaking through the gum one pair at a time, permanent teeth pushing them out from below, back molars arriving last, then gums receding and chewing surfaces wearing down as the years add up. Click any tooth for its clinical name, its FDI number, when it arrives, and which tooth replaces it.
Available in English and Indonesian.
Source comments are written in Indonesian. Everything user-facing lives in the language dictionaries, never in the code.
Most tooth-eruption resources are tables. A table tells you the lower second primary molar erupts at 23–31 months, but it cannot show you that it arrives after the canine, or that it sits in a slot which will be taken over by a premolar rather than a molar.
So the whole app is built around a single control. Age is the only input; everything else is derived from it:
flowchart LR
A["age<br/>(months)"] --> B["dentitionAt()<br/>lib/lifecycle.ts"]
B --> C["52 × ToothState<br/>phase · emergence<br/>recession · attrition"]
C --> D["toothPose()<br/>lib/pose.ts"]
C --> E["counts, life stage,<br/>accessible summary"]
D --> F["3D scene"]
E --> G["panels & tables"]
Because the 3D scene, the side panels, the schedule tables, and the screen-reader summary are all derived from the same pure function, they cannot drift out of sync with each other.
- Age scrubber, 0 → 90 years, on a stepped scale so infancy gets 42% of the slider instead of the 3% a linear scale would give it
- Playback that walks the full lifetime in about 52 seconds
- Four camera views — front, side, and an occlusal view of each jaw; pressing the button of the view you are already in resets the camera
- Jaw opening animated the way it actually works: the mandible rotates on its joint behind the last molar, while the upper jaw stays fixed to the skull
- Per-tooth interaction — click for details, jump to the age a tooth erupts, or follow the link between a baby tooth and its successor
- Teeth still inside the bone drawn faintly, so you can see permanent teeth waiting beneath the baby teeth
- Optional tooth-loss scenario for later life, off by default — see Accuracy and limits
- A full text equivalent of the 3D scene: schedule tables and a per-age tooth list that work without WebGL and read correctly in a screen reader
Requires Node 20 or newer; developed on 24.11.
npm install
npm run dev # http://localhost:3000 → redirects to /en| Script | What it does |
|---|---|
npm run dev |
Development server |
npm run build |
Production build; prerenders all four pages |
npm run start |
Serve the production build |
npm run lint |
ESLint |
Deployed on Vercel at https://learn-teeth-anatomy.vercel.app. Nothing about the build is host-specific: every page is static HTML, and the only server-side behaviour is the redirect from /.
The language is always the first URL segment, so every page states its own locale — which is what hreflang needs, and what makes a shared link unambiguous.
| Route | |
|---|---|
/ |
Redirects to /en — a temporary redirect, set in next.config.ts |
/en, /id |
The explorer |
/en/about, /id/about |
Sources, method notes, and limits |
All four pages are prerendered as static HTML at build time. There is no i18n library and no runtime locale detection.
The layering matters more than any individual file:
| Layer | Holds | Deliberately does not hold |
|---|---|---|
lib/teeth.ts |
The 52 teeth: FDI and Universal numbering, eruption and shedding ranges, dimensions, root and cusp counts | Any user-facing word |
lib/lifecycle.ts |
Age → the state of every tooth; ageing curves; life-stage classification | Any user-facing word |
lib/toothGeometry.ts, lib/pose.ts |
Procedural tooth shapes, dental-arch layout, placement in the scene | — |
lib/i18n/ |
Every word a visitor reads, in both languages | Numbers, timings, geometry |
Tooth text is looked up by a stable key such as permanent-lower-8, which keeps lib/teeth.ts purely numeric — identical in any language.
Dictionary is derived from the English dictionary via typeof, and tooth keys form a 26-member union type. A missing or misspelled key fails tsc rather than quietly rendering as empty text.
Tooth shapes are built in code from six numbers each — width, thickness, crown height, root length, root count, cusp count — and only 26 unique geometries are created, then reused across all 52 teeth.
The honest trade-off: the shapes are representative, not anatomically exact. Good enough to recognise which tooth is which, where it sits, and the order in which things happen. Not good enough for dental coursework. In return there is no large asset to download, and every tooth is an individual object that can be animated, numbered, coloured by phase, and clicked.
Swapping in real glTF meshes would mean replacing one function — toothGeometry(tooth) — and nothing else.
ARCH_SHAPE defines only the shape of the dental arch. Its actual size is computed so that the arc length exactly fits the sum of the crown widths, and teeth are then distributed along the arc by length rather than by curve parameter. Getting this wrong is what produced a 3 mm overlap at the canine bend during development. The derived result — roughly 67 × 49 mm — matches an adult arch carrying 32 teeth.
The palette is light only, and that took more than swapping colours:
- The 3D stage sits on a soft cool-grey gradient rather than white, because cream-white enamel disappears against white. Hue contrast does the work that value contrast cannot.
- Phase colours exist twice:
PHASE_COLORfor the 3D materials, following real enamel, andPHASE_INKfor text and badges, each at 4.5:1 contrast or better. The near-white "in full use" colour is unusable as text. - Translucent teeth use higher opacity than a dark theme would need, because half-transparent objects vanish much faster over a light background.
app/
[lang]/
layout.tsx Root layout — lives here so <html lang> can vary
page.tsx The explorer page
about/page.tsx Sources, method, limits
globals.css Palette and the 3D stage backdrop
icon.svg Favicon, designed for 16 px
components/
Explorer.tsx Owns age, view and selection state
AgeSlider.tsx Age scrubber, playback, milestone shortcuts
ToothDetail.tsx Per-tooth panel
EruptionReference.tsx Schedule tables — also the scene's text equivalent
LanguageSwitcher.tsx Switches locale while staying on the same page
SiteHeader.tsx Shared header
Emphasis.tsx Renders **bold** / *italic* from dictionary text
scene/
DentalScene.tsx Canvas, lighting, camera rig, jaw hinge
ToothMesh.tsx One tooth
GumArch.tsx Gum ridge, including age-related recession
lib/
teeth.ts The 52 teeth (no words)
lifecycle.ts Age → the state of every tooth
toothGeometry.ts Procedural shapes and arch layout
pose.ts Placing a tooth in the scene
ageScale.ts The stepped slider scale
sources.ts Reference list; titles are not translated
i18n/ Locales, dictionaries, age formatting
Worth reading before reusing anything here.
- Educational, not medical advice. The content describes the pattern that holds for most people. A few months' variation in a child is ordinary. Only a dentist examining one particular mouth can say anything about that mouth.
- Eruption ranges are population ranges. The app also uses each range as the duration of eruption, which makes the animation easy to follow but is not how an eruption chart should be read. For one individual, the range states when a tooth is likely to appear.
- Tooth shapes are simplified — see above.
- Losing all your teeth is not a normal part of ageing. It is the outcome of untreated gum disease and decay. That scenario is therefore opt-in, never the default view, and its timings are illustrative estimates rather than predictions.
- The crowding visible during mixed dentition is an illustration — a side effect of adult-sized teeth arriving in a still-growing jaw, not an orthodontic simulation.
Eruption and shedding ranges follow the American Dental Association eruption charts, with care guidance from the American Academy of Pediatric Dentistry and the American Academy of Pediatrics. Numbering follows FDI / ISO 3950. The full list with links is on the About page, and in lib/sources.ts.
Created by Ruston Efendi. You are welcome to download it, read it, take it apart, and build on it — that is what it is here for. The one thing asked in return is that people can still tell where it came from.
| Licence | ||
|---|---|---|
| Source code | MIT | Keep the copyright notice |
| Written content, prose, icon | CC BY 4.0 | Credit visibly, and say if you changed it |
The split exists because the two are different kinds of work. A code licence is a poor fit for several thousand words of explanatory prose, and the prose is a large part of this project.
If you reuse the content — the explanations, the phase descriptions, the teething guidance — a visible line is what CC BY asks for:
Content adapted from Tooth Atlas by Ruston Efendi, used under CC BY 4.0.
https://learn-teeth-anatomy.vercel.app
If you only reuse code, MIT does not require a visible credit. A link back is appreciated rather than required.
One thing that is nobody's to license: the dental facts themselves. Eruption ranges come from published ADA and AAPD charts, and facts cannot be copyrighted. What is licensed here is their arrangement and the wording used to explain them.
Next.js 16 (App Router, Turbopack) · React 19 · TypeScript 5 · Tailwind CSS 4 · three.js 0.185 with React Three Fiber 9 and drei 10.
No 3D assets, no image assets, no i18n runtime. Client JavaScript is roughly 428 KB gzipped, nearly all of it three.js and drei.