Interactive 3D Globe
A stunning, interactive WebGL globe for visualizing global data, server locations, or user activity.
Installation
Add this component to your project using the CLI:
npx vui-registry-cli-v1 add interactive-3d-globeSource Code
"use client"
import createGlobe from "cobe"
import { useEffect, useRef, useState } from "react"
import { motion } from "framer-motion"
import { MapPin, Activity, Wifi, RotateCw } from "lucide-react"
import { cn } from "@/lib/utils"
export default function Interactive3dGlobe() {
const canvasRef = useRef<HTMLCanvasElement>(null)
const pointerInteracting = useRef<number | null>(null)
const pointerInteractionMovement = useRef(0)
const rRef = useRef(0)
const [activeRegion, setActiveRegion] = useState("Americas")
const [width, setWidth] = useState(0)
const targetPhiRef = useRef(0)
const [isAutoRotating, setIsAutoRotating] = useState(true)
const isAutoRotatingRef = useRef(true)
const regions = {
"Americas": [40, -100],
"Europe": [50, 10],
"Asia": [30, 100],
"Africa": [10, 20],
"India": [20, 77]
}
const handleRegionChange = (name: string, coords: number[]) => {
setActiveRegion(name)
setIsAutoRotating(false)
isAutoRotatingRef.current = false
const target = coords[1] * Math.PI / 180
targetPhiRef.current = target
// Reset manual rotation offset so we go exactly to the target
rRef.current = 0
pointerInteractionMovement.current = 0
}
const updateWidth = () => {
if (canvasRef.current) {
setWidth(canvasRef.current.offsetWidth)
}
}
useEffect(() => {
window.addEventListener('resize', updateWidth)
updateWidth()
return () => {
window.removeEventListener('resize', updateWidth)
}
}, [])
useEffect(() => {
let currentPhi = 0;
let width = 0;
const onResize = () => canvasRef.current && (width = canvasRef.current.offsetWidth)
window.addEventListener('resize', onResize)
onResize()
if (!canvasRef.current) return
const globe = createGlobe(canvasRef.current, {
devicePixelRatio: 2,
width: width * 2,
height: width * 2,
phi: 0,
theta: 0.3,
dark: 1,
diffuse: 1.2,
mapSamples: 16000,
mapBrightness: 6,
baseColor: [0.3, 0.3, 0.3],
markerColor: [0.1, 0.8, 1],
glowColor: [1, 1, 1],
markers: [
{ location: [37.7595, -122.4367], size: 0.05 }, // San Francisco
{ location: [40.7128, -74.006], size: 0.05 }, // New York
{ location: [51.5074, -0.1278], size: 0.05 }, // London
{ location: [35.6762, 139.6503], size: 0.05 }, // Tokyo
{ location: [28.6139, 77.2090], size: 0.05 }, // New Delhi
{ location: [-33.8688, 151.2093], size: 0.05 }, // Sydney
{ location: [30.0444, 31.2357], size: 0.05 }, // Cairo
{ location: [55.7558, 37.6173], size: 0.05 }, // Moscow
{ location: [-23.5505, -46.6333], size: 0.05 }, // Sao Paulo
],
onRender: (state) => {
// This runs on every animation frame
if (!pointerInteracting.current) {
// Not dragging
if (isAutoRotatingRef.current) {
// Auto rotate
currentPhi += 0.003
targetPhiRef.current = currentPhi
} else {
// Smoothly go to target region
const dist = targetPhiRef.current - currentPhi
currentPhi += dist * 0.08
}
} else {
// When dragging, we pause updating currentPhi from auto-rotation/target
// The visual rotation is handled by rRef
// Optionally update targetPhiRef to match current visual state so when we release it doesn't snap
// But here we keep it simple: currentPhi stays put, rRef adds offset.
}
state.phi = currentPhi + rRef.current
state.width = width * 2
state.height = width * 2
}
})
setTimeout(() => (canvasRef.current!.style.opacity = '1'))
return () => globe.destroy()
}, [])
return (
<div className="relative flex h-[700px] w-full flex-col items-center justify-center overflow-hidden rounded-3xl bg-background font-sans">
{/* Cinematic Background */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-primary/10 via-foreground/5 to-transparent" />
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-10 mix-blend-overlay" />
{/* Grid Overlay */}
<div className="absolute inset-0 bg-[linear-gradient(hsl(var(--foreground)/0.05)_1px,transparent_1px),linear-gradient(90deg,hsl(var(--foreground)/0.05)_1px,transparent_1px)] bg-[size:100px_100px] [mask-image:radial-gradient(ellipse_at_center,black_40%,transparent_70%)] pointer-events-none" />
{/* HUD UI Layer */}
<div className="absolute top-8 left-8 z-10 hidden md:block">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-blue-400">
<Activity className="h-4 w-4 animate-pulse" />
<span className="text-xs font-mono uppercase tracking-widest">System Status: Nominal</span>
</div>
<div className="flex items-center gap-2 text-emerald-400">
<Wifi className="h-4 w-4" />
<span className="text-xs font-mono uppercase tracking-widest">Network: 10.5 Gbps</span>
</div>
</div>
</div>
{/* Globe Container */}
<div className="relative z-0 h-full w-full flex items-center justify-center">
<canvas
ref={canvasRef}
style={{ width: 600, height: 600, maxWidth: "100%", aspectRatio: 1 }}
className="opacity-0 transition-opacity duration-1000"
onPointerDown={(e) => {
pointerInteracting.current = e.clientX - pointerInteractionMovement.current
canvasRef.current!.style.cursor = 'grabbing'
// Pause auto-rotation on interaction
setIsAutoRotating(false)
isAutoRotatingRef.current = false
}}
onPointerUp={() => {
pointerInteracting.current = null
canvasRef.current!.style.cursor = 'grab'
}}
onPointerOut={() => {
pointerInteracting.current = null
canvasRef.current!.style.cursor = 'grab'
}}
onMouseMove={(e) => {
if (pointerInteracting.current !== null) {
const delta = e.clientX - pointerInteracting.current
pointerInteractionMovement.current = delta
rRef.current = delta / 200
}
}}
onTouchMove={(e) => {
if (pointerInteracting.current !== null && e.touches[0]) {
const delta = e.touches[0].clientX - pointerInteracting.current
pointerInteractionMovement.current = delta
rRef.current = delta / 100
}
}}
/>
</div>
{/* Region Controls */}
<div className="absolute bottom-8 z-10 w-full px-4">
<motion.div
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ type: "spring", damping: 20, stiffness: 100 }}
className="mx-auto flex max-w-fit items-center justify-center gap-2 rounded-full border border-border bg-card/80 p-2 backdrop-blur-xl shadow-2xl"
>
{Object.entries(regions).map(([name, coords]) => (
<button
key={name}
onClick={() => handleRegionChange(name, coords)}
className={cn(
"group relative flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium transition-all duration-300",
activeRegion === name
? "bg-foreground text-background shadow-lg shadow-foreground/10 scale-105"
: "text-muted-foreground hover:bg-foreground/10 hover:text-foreground"
)}
>
<MapPin className={cn("h-3 w-3 transition-colors", activeRegion === name ? "text-blue-500" : "text-neutral-500 group-hover:text-white")} />
{name}
{activeRegion === name && (
<span className="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-blue-500 animate-ping" />
)}
</button>
))}
<div className="mx-2 h-6 w-px bg-white/10" />
<button
onClick={() => {
const newVal = !isAutoRotating
setIsAutoRotating(newVal)
isAutoRotatingRef.current = newVal
// Reset manual drag if we enable auto-rotate
if (newVal) {
rRef.current = 0
pointerInteractionMovement.current = 0
}
}}
className={cn(
"group relative flex items-center justify-center rounded-full p-2 transition-all duration-300",
isAutoRotating
? "bg-blue-500/20 text-blue-400"
: "text-neutral-400 hover:bg-white/10 hover:text-white"
)}
title="Auto Rotate"
>
<RotateCw className={cn("h-4 w-4", isAutoRotating && "animate-spin-slow")} />
</button>
</motion.div>
</div>
</div>
)
}
Dependencies
cobe: latestframer-motion: latestclsx: latesttailwind-merge: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| markers | Array<{ location: [number, number]; size: number }> | Built-in demo markers | Markers to render on the globe. |
| autoRotate | boolean | true | Enable gentle auto-rotation when idle. |
| className | string | undefined | Additional CSS classes. |
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.

