---
name: interactive-globe
description: Use when you want "Global, techy, premium" - Shows a rotating or clickable 3D globe.
---

# Interactive Globe

> **Category:** Advanced WebGL / Canvas  -  **Personality:** Global, techy, premium
>
> **Best use:** SaaS, logistics, analytics
>
> **Ratings:** Professional 5/5 - Casual 3/5 - Exotic 4/5 - Visual impact 5/5 - Difficulty 5/5 - Performance cost 5/5

## What it is

A real-time 3D globe built from thousands of points on a sphere (a "dot globe"), auto-rotating and draggable, with glowing arcs that fly "data packets" between city hubs. No earth texture is needed — the form is sold by a dense Fibonacci point-cloud, a dark inner sphere that occludes the far side, and a fresnel atmosphere rim. It's the signature hero for anything that lives on a map: edge networks, logistics fleets, global analytics. Because it is the heaviest effect in the catalog, it ships with a reduced-motion still frame and a pure-CSS fallback for when WebGL or the CDN is unavailable.

## Dependencies / CDN

three.js only (pinned UMD build that exposes the global `THREE`), with Subresource Integrity. Orbit/drag is hand-rolled, so no `OrbitControls` add-on is required (the legacy UMD `OrbitControls` no longer ships for r160 anyway).

```html
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"
        integrity="sha384-qOkzR5Ke/XkQxuGVJ9hpFEpDlcoLtWwVYhnJf06cLIZa2vaIptSqaubivErzmD5O"
        crossorigin="anonymous" referrerpolicy="no-referrer"></script>
```

Fonts (optional): **Sora** (display), **Manrope** (UI), **JetBrains Mono** (data/coords).

## HTML

A contained stage holds the canvas plus absolutely-positioned overlays, and a hidden CSS globe used only when WebGL fails.

```html
<div class="ig-stage" id="ig-stage">
  <div class="ig-glow" aria-hidden="true"></div>
  <canvas class="ig-canvas" id="ig-canvas" aria-label="Interactive 3D globe — drag to rotate"></canvas>

  <!-- overlays: brand bar, hero copy, glass telemetry panel, coord/hint -->
  <aside class="ig-panel">…live stats…</aside>
  <div class="ig-readout"><span class="ig-coord">VIEW <b id="ig-lat">11°N</b> · <b id="ig-lon">88°W</b></span></div>

  <!-- shown only when .ig-stage.is-fallback -->
  <div class="ig-fallback" aria-hidden="true"><div class="ig-fb"></div></div>
</div>
```

## CSS

The stage + canvas, and the CSS fallback globe (itself a masked dot pattern):

```css
.ig-stage{position:relative;border-radius:26px;overflow:hidden;isolation:isolate;
  min-height:clamp(560px,72vh,684px);
  background:radial-gradient(122% 92% at 72% 36%,#0c1a32,#070d1c 47%,#04060e 100%)}
.ig-canvas{position:absolute;inset:0;width:100%;height:100%;display:block;
  touch-action:none;cursor:grab}             /* touch-action:none → drag, not scroll */
.ig-canvas:active{cursor:grabbing}
.ig-glow{position:absolute;inset:auto 5% auto auto;width:62%;height:84%;top:9%;pointer-events:none;
  background:radial-gradient(circle,rgba(56,196,255,.22),transparent 68%)}

/* ---- WebGL / CDN fallback: a pure-CSS dot globe ---- */
.ig-fallback{display:none;position:absolute;inset:0;place-items:center}
.ig-stage.is-fallback .ig-canvas{display:none}
.ig-stage.is-fallback .ig-fallback{display:grid}
.ig-fb{position:relative;width:clamp(220px,42vw,360px);aspect-ratio:1;border-radius:50%;
  background:radial-gradient(circle at 36% 30%,rgba(127,233,255,.5),rgba(31,143,214,.2) 42%,rgba(7,17,32,.95) 72%),#060d1a;
  box-shadow:inset 0 0 60px rgba(4,10,22,.92),0 0 70px rgba(56,196,255,.26);overflow:hidden}
.ig-fb::before{content:"";position:absolute;inset:0;border-radius:50%;
  background-image:radial-gradient(rgba(127,225,255,.55) 1.1px,transparent 1.4px);background-size:13px 13px;
  -webkit-mask-image:radial-gradient(circle at 36% 34%,#000 56%,transparent 82%);
          mask-image:radial-gradient(circle at 36% 34%,#000 56%,transparent 82%);
  animation:ig-spin 28s linear infinite}
@keyframes ig-spin{to{transform:rotate(360deg)}}
@media(prefers-reduced-motion:reduce){.ig-fb::before{animation:none}}
```

## JavaScript

The real demo code, trimmed to the load-bearing parts. Guard first, then build the globe, arcs and a hand-rolled orbit.

```js
(function(){
  var stage=document.getElementById('ig-stage'), canvas=document.getElementById('ig-canvas');
  var reduced=matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 1) GUARD: CDN/SRI failure (no global THREE) or no WebGL → CSS fallback, bail out.
  function webglOK(){try{var c=document.createElement('canvas');
    return !!(window.WebGLRenderingContext&&(c.getContext('webgl')||c.getContext('experimental-webgl')));}catch(e){return false;}}
  if(!window.THREE||!webglOK()){ stage.classList.add('is-fallback'); return; }

  var THREE=window.THREE, R=1;
  var renderer=new THREE.WebGLRenderer({canvas:canvas,antialias:true,alpha:true});
  renderer.setPixelRatio(Math.min(devicePixelRatio||1,2)); renderer.setClearColor(0x000000,0);
  var scene=new THREE.Scene(), camera=new THREE.PerspectiveCamera(40,1,0.1,100);
  camera.position.z=4; var globe=new THREE.Group(); scene.add(globe);

  // round soft sprite so every point is a glowing dot, not a square
  function dotTex(){var s=64,cv=document.createElement('canvas');cv.width=cv.height=s;var g=cv.getContext('2d');
    var r=g.createRadialGradient(32,32,0,32,32,32);r.addColorStop(0,'#fff');r.addColorStop(.4,'rgba(255,255,255,.8)');r.addColorStop(1,'rgba(255,255,255,0)');
    g.fillStyle=r;g.fillRect(0,0,s,s);return new THREE.CanvasTexture(cv);}
  var sprite=dotTex();

  // 2) DOT GLOBE: evenly-spread points via the Fibonacci sphere
  var N=2600,pos=new Float32Array(N*3),gold=Math.PI*(3-Math.sqrt(5));
  for(var i=0;i<N;i++){var y=1-(i/(N-1))*2,rr=Math.sqrt(1-y*y),t=gold*i;
    pos[i*3]=Math.cos(t)*rr*R; pos[i*3+1]=y*R; pos[i*3+2]=Math.sin(t)*rr*R;}
  var dg=new THREE.BufferGeometry(); dg.setAttribute('position',new THREE.BufferAttribute(pos,3));
  globe.add(new THREE.Points(dg,new THREE.PointsMaterial({size:.021,map:sprite,color:0x74e7ff,
    transparent:true,depthWrite:false,sizeAttenuation:true})));
  // dark inner sphere occludes far-side dots → reads as a solid globe
  globe.add(new THREE.Mesh(new THREE.SphereGeometry(R*0.985,48,48),new THREE.MeshBasicMaterial({color:0x060c18})));

  // 3) ATMOSPHERE: back-side fresnel rim (the cyan halo)
  globe.add(new THREE.Mesh(new THREE.SphereGeometry(R*1.17,48,48),new THREE.ShaderMaterial({
    uniforms:{uColor:{value:new THREE.Color(0x3ac6ff)}},
    vertexShader:'varying vec3 vN;void main(){vN=normalize(normalMatrix*normal);gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}',
    fragmentShader:'varying vec3 vN;uniform vec3 uColor;void main(){float i=pow(clamp(.62-dot(vN,vec3(0,0,1.)),0.,1.),3.);gl_FragColor=vec4(uColor,1.)*i;}',
    side:THREE.BackSide,blending:THREE.AdditiveBlending,transparent:true,depthWrite:false})));

  // 4) ARCS: lat/lon → vec3, bow a bezier above the surface, slide a bright "packet" along it
  function ll(lat,lon,r){var p=(90-lat)*Math.PI/180,th=(lon+180)*Math.PI/180;
    return new THREE.Vector3(-r*Math.sin(p)*Math.cos(th),r*Math.cos(p),r*Math.sin(p)*Math.sin(th));}
  var arcs=[];
  function arc(a,b,color){
    var pa=ll(a[0],a[1],R*1.004), pb=ll(b[0],b[1],R*1.004);
    var mid=pa.clone().add(pb).multiplyScalar(.5);
    mid.normalize().multiplyScalar(R*(1+Math.min(.55,pa.distanceTo(pb)*.42)));   // lift longer arcs higher
    var samp=new THREE.QuadraticBezierCurve3(pa,mid,pb).getSpacedPoints(120);
    globe.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(samp),
      new THREE.LineBasicMaterial({color:color,transparent:true,opacity:.16,blending:THREE.AdditiveBlending,depthWrite:false})));
    var CN=18,cp=new Float32Array(CN*3),cg=new THREE.BufferGeometry();
    cg.setAttribute('position',new THREE.BufferAttribute(cp,3));
    globe.add(new THREE.Points(cg,new THREE.PointsMaterial({size:.05,map:sprite,color:color,
      transparent:true,depthWrite:false,sizeAttenuation:true,blending:THREE.AdditiveBlending})));
    arcs.push({samp:samp,S:samp.length-1,cp:cp,cg:cg,CN:CN,t:Math.random(),speed:.0015+Math.random()*.0016});
  }
  arc([37.77,-122.42],[40.71,-74],0x67e8ff); arc([51.51,-.13],[1.35,103.82],0x67e8ff);
  arc([25.2,55.27],[19.08,72.88],0xffc24b);  /* …add as many routes as you like… */

  function comets(){for(var a=0;a<arcs.length;a++){var ar=arcs[a];ar.t=(ar.t+ar.speed)%1;
    for(var k=0;k<ar.CN;k++){var f=ar.t-(1-k/(ar.CN-1))*.16; if(f<0)f+=1;
      var p=ar.samp[Math.min(ar.S,Math.floor(f*ar.S))]; ar.cp[k*3]=p.x;ar.cp[k*3+1]=p.y;ar.cp[k*3+2]=p.z;}
    ar.cg.attributes.position.needsUpdate=true;}}
  comets();

  // 5) HAND-ROLLED ORBIT: drag + flick inertia + gentle auto-spin (auto = 0 under reduced-motion)
  var curY=.5,curX=-.18,tgtY=.5,tgtX=-.18,velY=0,velX=0,drag=false,lx,ly,auto=reduced?0:.0016;
  var clamp=function(v){return Math.max(-1.05,Math.min(1.05,v));};
  canvas.addEventListener('pointerdown',function(e){drag=true;lx=e.clientX;ly=e.clientY;velY=velX=0;canvas.setPointerCapture(e.pointerId);});
  canvas.addEventListener('pointermove',function(e){if(!drag)return;
    velY=(e.clientX-lx)*.005; velX=(e.clientY-ly)*.005; lx=e.clientX; ly=e.clientY;
    tgtY+=velY; tgtX=clamp(tgtX+velX); if(reduced){curY=tgtY;curX=tgtX;render();}});
  canvas.addEventListener('pointerup',function(){drag=false;});

  function render(){globe.rotation.y=curY;globe.rotation.x=curX;renderer.render(scene,camera);}
  function resize(){var w=stage.clientWidth,h=stage.clientHeight;renderer.setSize(w,h,false);
    camera.aspect=w/h;camera.updateProjectionMatrix();if(reduced)render();}
  function loop(){requestAnimationFrame(loop);
    if(!drag){tgtY+=velY;tgtX=clamp(tgtX+velX);velY*=.94;velX*=.94;tgtY+=auto;}
    curY+=(tgtY-curY)*.1; curX+=(tgtX-curX)*.1; comets(); render();}
  new ResizeObserver(resize).observe(stage); resize();
  reduced ? render() : loop();      // reduced-motion: ONE static frame, no auto-spin / no packet loop
})();
```

## How it works

- **Fibonacci sphere** spaces `N` points almost perfectly evenly across the sphere (golden-angle increment), giving the clean "dot globe" with no clumping at the poles.
- A **dark inner sphere** at `R*0.985` is opaque and writes depth, so the transparent far-side dots fail the depth test and disappear — that occlusion is what turns a transparent point-cloud into a believable solid globe.
- The **atmosphere** is a slightly larger back-side sphere; a fresnel term (`pow(0.62 - dot(normal, viewDir), 3)`) lights only the grazing edge, additively blended, for the halo.
- **Arcs** convert each city's lat/lon to a vector, then draw a `QuadraticBezierCurve3` whose control point is the midpoint pushed outward (longer hops arc higher). A faint full line is the route; a short strip of points whose positions are rewritten each frame is the travelling "packet".
- **Orbit** rotates the globe *group* (camera stays put). Pointer deltas feed a target rotation; each frame eases `current → target`, adds a tiny auto-spin, and decays leftover drag velocity for flick inertia.

## Customization

- **Density / look:** raise `N` for a finer globe (cost scales linearly); switch the `Points` for a `WireframeGeometry` if you want a lattice instead of dots.
- **Continents:** sample a value-noise (or an equirectangular land mask) at each point and brighten/enlarge "land" dots — the live demo uses a 2-octave value-noise for organic density.
- **Routes:** edit the city list and the `arc(a,b,color)` calls; colour packets per tier (cyan edge, violet backbone, amber priority). Tune `speed` and the `0.16` tail length.
- **Framing:** `camera.position.z` (smaller = bigger globe) and `globe.position.x` to nudge it off-centre for hero copy.
- **Palette:** the dot `color`, atmosphere `uColor`, and stage gradient are the three knobs that set the whole mood.

## Accessibility & performance

- **Reduced motion:** `auto` spin is zeroed, the rAF loop never starts, and one static frame is rendered (dragging still re-renders on demand). CSS pulses/spins are disabled via `@media (prefers-reduced-motion: reduce)`.
- **WebGL / CDN failure:** the opening guard (`!window.THREE || !webglOK()`) swaps in a pure-CSS dot globe so the section never goes blank — verified by pointing the `<script src>` at a 404.
- **Cost control:** `setPixelRatio(min(dpr,2))` caps retina overdraw; geometry/materials are built once and only the small packet buffers (≈18 points × routes) update per frame; `getSpacedPoints` is precomputed so no curve maths runs in the loop.
- **Input:** `touch-action:none` on the canvas lets pointer-drag work on touch without hijacking page scroll; the canvas carries an `aria-label` and all real content lives in HTML overlays, not the canvas.

## Gotchas

- **`three.min.js` is the UMD global build.** It logs an r150+ deprecation warning (harmless) and is the *only* SRI-able global build — the ESM build needs an import-map, which can't carry SRI. Don't "fix" the warning by switching to modules unless you drop the SRI requirement.
- **Square dots?** `PointsMaterial` renders squares by default — you must supply a radial-gradient sprite as `map` with `transparent:true` and `depthWrite:false` to get round, soft points.
- **Far-side bleed-through.** Without the opaque inner occluder sphere, you see dots and arcs from the back of the globe and it looks flat. The occluder (depth-writing) is mandatory.
- **Additive arcs need `depthTest` on** (the default) so packets are correctly hidden when they pass behind the globe — otherwise they glow straight through it.
- **Capture the pointer.** `setPointerCapture` keeps a drag alive when the cursor leaves the canvas; without it a fast drag "sticks".
- **Size the renderer to the element, not the window.** Use a `ResizeObserver` on the stage and `setSize(w,h,false)` (the `false` keeps your CSS sizing) — reading `innerWidth` breaks inside a constrained, rounded container.
