A lightweight, highly-configurable interactive 3D globe built with Three.js and Canvas 2D.
No external map dependencies β everything is self-contained in a single HTML file. Designed to embed directly in iOS (WKWebView), Android (WebView), or any web page.
π https://mibrahim017.github.io/3d-globe-map/
- π All 250+ countries rendered from SVG paths directly onto a 3D sphere texture
- π¨ Fully themeable β colors, ocean, grid, glow, stroke, all configurable
- π± Mobile-first β adaptive texture resolution, touch drag, pinch-zoom
- π Smooth camera β animated zoom-to-country, slerp rotation, inertia-free drag
- π‘ Native bridge β iOS WKWebView, Android JSInterface, and web
windowcallbacks - β‘ Zero npm β single HTML file, one Three.js CDN import
- π Rich JS API β colorize countries, swap SVGs, control camera programmatically
<iframe src="src/globe3d.html" style="width:100%;height:500px;border:none"></iframe>Just open src/globe3d.html in any modern browser.
let webView = WKWebView(frame: view.bounds, configuration: config)
let url = Bundle.main.url(forResource: "globe3d", withExtension: "html")!
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(MyBridge(), "Android")
webView.loadUrl("file:///android_asset/globe3d.html")globe3d/
βββ src/
β βββ globe3d.html # β The globe (self-contained)
β βββ three.min.js # Three.js r128 (copy from CDN or npm)
βββ demo/
β βββ index.html # Full interactive configuration demo (8 tabs)
βββ examples/
β βββ android/
β β βββ README.md # Android setup guide
β β βββ app/src/main/
β β βββ AndroidManifest.xml
β β βββ assets/ # β copy globe3d.html + three.min.js here
β β βββ java/com/globe3d/example/
β β β βββ GlobeActivity.java # Java integration (full example)
β β β βββ GlobeActivityKt.kt # Kotlin integration (full example)
β β βββ res/layout/
β β βββ activity_globe.xml # WebView layout with overlay UI
β βββ ios/
β βββ README.md # iOS setup guide
β βββ Globe3DExample/
β βββ GlobeViewController.swift # UIKit / Swift (full example)
β βββ GlobeSwiftUI.swift # SwiftUI wrapper + GlobeController
β βββ GlobeViewController.m # UIKit / Objective-C (full example)
β βββ GlobeViewController.h # Objective-C header
βββ docs/
β βββ API.md # Complete API reference
βββ README.md
βββ LICENSE
Note:
globe3d.htmlreferencesthree.min.jsvia<script src="three.min.js">.
Copy it from cdnjs intosrc/.
All configuration lives in clearly marked constant blocks at the top of globe3d.html.
Edit them before deploy, or override them at runtime via the JS API.
// ββ Colour config ββββββββββββββββββββββββββββββββββββββββββββββββββ
var COUNTRY_FILL = '#2a6496'; // Default country fill
var COUNTRY_HOVER = '#4a9fd4'; // Hovered country fill
var COUNTRY_STROKE = 'rgba(0,0,0,0.35)'; // Country border color
var SELECT_STROKE = 'rgba(255,255,255,0.9)'; // Selected country borderThe ocean is drawn as a Canvas 2D linear gradient inside buildBg():
function buildBg(){
var g = _bgCtx.createLinearGradient(0, 0, 0, TEX_H);
g.addColorStop(0, '#8ee0d8'); // β Ocean top color
g.addColorStop(.5, '#6dccc4'); // β Ocean mid color
g.addColorStop(1, '#8addd6'); // β Ocean bottom color
_bgCtx.fillStyle = g;
_bgCtx.fillRect(0, 0, TEX_W, TEX_H);
// Grid lines
_bgCtx.strokeStyle = 'rgba(255,255,255,0.18)'; // β grid opacity
_bgCtx.lineWidth = 0.4;
// ... draws 30Β° graticule
}var FOV = isMobile ? 55 : 42; // Field of view (degrees)
var GLOBE_R = 1.0; // Globe radius (Three.js units)
// Auto-computed from FOV β override if needed:
var FIT_Z = GLOBE_R / Math.sin(_fovRad / 2); // camera distance = full fit
var NEAR_Z = FIT_Z * 0.68; // zoom-in distance (68% of fit)
var ZOOM_OUT_Z = FIT_Z * 1.05; // default zoom-out (slight pull-back)
var ZOOM_IN_Z = NEAR_Z; // used when country is selected
var MIN_Z = 0.5; // Hard minimum zoom (scroll/pinch limit)
var MAX_Z = 5.5; // Hard maximum zoomvar autoSpeed = 0.15; // Rotation speed (0 = stopped, 1.5 = fast)
// Direction: always around Y-axis (eastβwest). To reverse, negate autoSpeed.var TEX_W = isMobile ? 2048 : 4096; // Texture width in pixels
var TEX_H = isMobile ? 1024 : 2048; // Texture height in pixels
// Globe mesh segments (higher = smoother sphere):
var globe = new THREE.Mesh(
new THREE.SphereGeometry(GLOBE_R, isMobile?56:80, isMobile?40:56),
globeMat
);All functions are globally accessible on window (or iframe.contentWindow).
| Function | Description |
|---|---|
setCountryColor(id, color) |
Set fill of one country by ISO-3166-1 alpha-2 code |
colorize(pathOrId, color, strokeColor?) |
Alias with optional stroke |
setupCountry(ids[], selColor, fill, bg, stroke) |
Batch setup β selected countries + all colors |
setupCountries(ids[], selColor, fgColor) |
Lighter version without background |
loadSVG(rawSVG, ids[], selColor, bg, fg) |
Replace SVG paths at runtime |
setMapBackground(color) |
Change body/canvas background |
getRandomColor() |
Returns a random hex color string |
| Function | Description |
|---|---|
zoomToCountry(idOrElement) |
Smooth-rotate globe so country faces camera (+Z) |
animateZoom(targetZ, onDone?) |
Ease camera to Z distance over 40 frames |
setZoom(z) |
Instantly set camera Z (clamped to MIN_Z..MAX_Z) |
zoomIn() |
Step in by 0.4 units |
zoomOut() |
Step out by 0.4 units |
pauseGlobe() |
Stop auto-rotation |
resumeGlobe() |
Resume auto-rotation |
resetView() |
Reset orientation to identity + zoom out |
deselectAll() |
Clear selection and restore rotation |
| Function | Description |
|---|---|
toggleCountry(pathElement) |
Select/deselect a country (internal, but safe to call) |
sel |
Set<string> of currently selected country IDs |
customColors |
{ [id]: hexColor } β override per-country fill |
customStrokes |
{ [id]: color } β override per-country stroke |
Globe fires events to three bridge types simultaneously. Implement whichever fits your platform:
// Inject before / after globe loads:
iframe.contentWindow.NativeBridge = {
didTapPath: function(jsonString) {
var data = JSON.parse(jsonString);
// data = { id, selected, centroid:{x,y}, bounds:{x,y,width,height}, color, stroke }
console.log('Tapped:', data.id, data.selected);
}
};// In parent page β called ~200ms after globe init:
window.onGlobeReady = function() {
console.log('Globe is ready');
};// In WKScriptMessageHandler:
func userContentController(_ ucc: WKUserContentController,
didReceive message: WKScriptMessage) {
if message.name == "didTapPath" {
let json = message.body as! String
// parse json: { id, selected, centroid, bounds, color, stroke }
}
if message.name == "globeReady" {
// globe is loaded and ready
}
}
// Register handlers:
config.userContentController.add(self, name: "didTapPath")
config.userContentController.add(self, name: "globeReady")Calling back into the globe from Swift:
webView.evaluateJavaScript("setCountryColor('US','#ff6b35')")
webView.evaluateJavaScript("zoomToCountry('JP')")class GlobeBridge {
@JavascriptInterface
fun onCountryTapped(json: String) {
val data = JSONObject(json)
val id = data.getString("id")
val selected = data.getBoolean("selected")
// handle on UI thread:
runOnUiThread { handleCountryTap(id, selected) }
}
@JavascriptInterface
fun onGlobeReady() {
// Globe initialized
}
}
webView.addJavascriptInterface(GlobeBridge(), "Android")Calling back into the globe:
webView.evaluateJavascript("setCountryColor('CN','#e63946')", null)COUNTRY_FILL = '#1a0a2e';
COUNTRY_STROKE = 'rgba(123,47,255,0.7)';
// Ocean:
g.addColorStop(0, '#0a0020');
g.addColorStop(1, '#050015');
document.body.style.background = '#050505';// Color each continent a distinct color
['US','CA','MX'].forEach(id => setCountryColor(id, '#4361ee'));
['CN','IN','JP'].forEach(id => setCountryColor(id, '#e76f51'));
['DE','FR','GB'].forEach(id => setCountryColor(id, '#457b9d'));// countries = [{id:'US', value:0.9}, {id:'CN', value:0.7}, ...]
function heatColor(t) {
var r = Math.round(255 * t);
var b = Math.round(255 * (1 - t));
return 'rgb('+r+',50,'+b+')';
}
countries.forEach(c => setCountryColor(c.id, heatColor(c.value)));
needRedraw = true;Every country tap fires this JSON:
| Browser | Support |
|---|---|
| Chrome 80+ | β Full |
| Safari 14+ | β Full |
| Firefox 80+ | β Full |
| iOS WKWebView 14+ | β Full |
| Android WebView (Chrome 80+) | β Full |
| Samsung Internet 12+ | β Full |
Requires: WebGL, Canvas 2D, ES6 (Set, const, arrow functions polyfilled for ES5 targets).
The globe references <script src="three.min.js">.
You can get it in several ways:
# Option 1: Download directly
curl -o src/three.min.js \
https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js
# Option 2: npm
npm install three@0.128.0
cp node_modules/three/build/three.min.js src/
# Option 3: Change to CDN in globe3d.html (no local file needed)
# Replace: <script src="three.min.js">
# With: <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js">- Fork the repo
- Create a branch:
git checkout -b feature/my-feature - Make changes to
src/globe3d.htmlordemo/ - Open a PR with a clear description
Areas welcome for contribution:
- Additional country data layers (population, GDP choropleth)
- Star/atmosphere shader layer
- Marker/pin system (lat/lng β 3D point)
- Offline SVG swap for custom regions
- React / Vue wrapper component
MIT β free for personal and commercial use. See LICENSE.
- Three.js β 3D rendering engine (r128)
- SVG world map paths adapted from Natural Earth public domain data
- Built with β€οΈ for cross-platform native + web use

{ "id": "US", // ISO 3166-1 alpha-2 "selected": true, // toggle state "centroid": { "x": 203.4, "y": 312.1 }, // SVG coordinate centroid "bounds": { "x": 50.2, "y": 290.0, "width": 310.5, "height": 120.3 // SVG bounding box }, "color": "#2a6496", // current fill (null if default) "stroke": null // current stroke override (null if default) }