Nusantara Languages
A map of Indonesia's 726 languages, linked to their family tree so each branch lights up its territory

- Indonesian languages on one linked map
- 726
- language families in the tree
- 56
- languages with a mapped speaker area
- 58%
- confusable colour pairs fixed for colour-blind readers
- 48 → 2
On this page
The problem
Indonesia has more than 700 languages, one of the densest concentrations on Earth. For most people, the shape of that diversity is invisible. Which languages are related? How did the families spread across the islands? Language maps exist, and family trees exist, but nothing links the two into one thing you can explore.
I built Nusantara Languages for students, linguists and anyone curious about where Indonesia's language families meet. The clearest example is the seam where the Austronesian and Papuan families touch, which runs through Halmahera. It is a personal, open-source project. It isn't Badan Bahasa's official "Peta Bahasa", so it doesn't use that name or look.
What I built
The map and the family tree are one object
The map and the tree read from the same data. Hover a branch of the tree and every territory in that group lights up on the map. Click a territory and the tree scrolls to it and opens its ancestry. Because both views read one structure, they can't fall out of sync. I wrote the tree and geometry logic as plain functions, so the trickiest parts can be tested without a browser.
I measured speed before styling anything
The site is static and makes no network requests at runtime. A build step fetches the worldwide sources, keeps Indonesia, simplifies the shapes and writes one data bundle with a coverage report. Before any styling, I measured whether hovering would feel instant with 726 languages drawn. It came in at about 0.01 milliseconds against a 2-millisecond budget. That let me keep plain vector drawing and hand-build both the map and the tree parser.
I dropped the planned atlas over its licence
The plan was to draw territories from a 1980s atlas, the source meant to make the map look good. At the licence check, it turned out to allow non-commercial use only. That conflicts with the open licence the site's data is published under. I refused the source, recorded why on the method page, and switched to two openly licensed datasets from 1990 to 2020. The map's era changed with it. Ethnologue is banned entirely, and a test proves none of its data ships. Where a figure exists only there, the page leaves it out and says so.
A language without a territory stays a point
Of the 726 languages, 421 have mapped speaker areas. The other 305 appear as labelled points, drawn differently. I didn't stretch them into shapes to make the map look complete. The build computes that 58% figure from the data, so it can't drift. The coastline comes from a geographic dataset and is never traced from language areas. I chose family colours to stay distinct for three kinds of colour blindness, and show endangerment as hatching over the family colour.
Result
Nusantara Languages is live in English and Indonesian, fully static, and works offline. Beyond the map and tree, it answers three questions from the same data. Which languages are a language's closest relatives? How far does a family spread? Austronesian spans about 5,010 km, Timor-Alor-Pantar about 350 km. Which languages from different families touch? There are 67 such pairs. Each answer prints its rule and its limit next to the number. You can also colour by subgroup, follow guided views of the seams, and export a PNG with attribution built in, as the licence requires. The site states that boundaries between languages are gradual, and how many languages are areas and how many are points. I built it alone between August and September 2026.
Under the hoodTechnical detail for engineers
- Next.js static export on GitHub Pages; React, strict TypeScript, Tailwind, Zod for the source manifest, Vitest,
tsxfor pipeline scripts, pnpm. Type is Newsreader for display and prose, IBM Plex Sans Condensed for labels, IBM Plex Mono for data and counts, all self-hosted throughnext/font. - Two browser-only bugs slipped past the type checker and the test suite. A
<style>tag with a text child broke hydration, fixed by switching todangerouslySetInnerHTML. Grouping land by geometry kind silently dropped 273 minor islands; the fix groups by whether a shape has a neighbour instead. - No mapping, GIS, Newick or topology library. The Newick parser and the SVG plate with its hover hit-testing are hand-written;
lib/treeandlib/geoare pure functions tested in Node. sources:buildfetches, licence-gates, filters to Indonesia, simplifies polygons, cross-checks lineages and writes the bundle pluscoverage.json. Raw worldwide dumps are never committed.- M0 render budget: 19,570 vertices against 60,000, hover about 0.01 ms against 2 ms, so SVG stayed and canvas wasn't needed.
- The licence gate needs every source to declare a pinned, machine-checked licence. Wurm & Hattori's atlas (republished as GeoJSON by Glottography) is CC-BY-NC-4.0, incompatible with the bundle's CC-BY-SA-4.0, and sits in the manifest as
decision: 'refused'. Replacements:asher2007worldandschapper2020papuan, both CC-BY-4.0.test:licenceasserts no Ethnologue-derived field exists. - Point-only languages use Glottolog coordinates, which are often population midpoints. Convex hulls and Voronoi fills are banned by a tested invariant. The Natural Earth coastline was added back mid-project after islands looked "eaten away"; it is
pointer-events-none. - Family colours come from a curated OKLCH set scored against three colour-vision deficiencies; confusable pairs fell from 15 to 0 in normal vision. Saturation is reserved for the current selection.
- Accessibility: the tree is a
role="tree"with roving tabindex, one tab stop instead of 1,716 when fully expanded. Several ARIA roles without matching keyboard behaviour (tablist,combobox) were removed in favour of native elements. - The flaky gate uses
Math.max(...samples)against 16 ms; observed runs ranged from 0.57 ms to 27.34 ms. - 726 languages, 56 top-level families, 23 isolates. Contact pairs come from a segment-to-segment adjacency check. JS budget: 234 KB gzipped excluding geometry, measured against a 250 KB target.
- About 21,135 lines of TypeScript, 579 tests across 37 test files, 18 components, about 10 routes, 112 commits from 12 August to 27 September 2026.
Screenshots
Hiring, or building something similar?
I'm open to full-time roles and select freelance work.