Interactive 3D Globe
A stunning, interactive WebGL globe for visualizing global data, server locations, or user activity.
Preview
Live interactive preview.
Installation
Add this component to your project using the CLI:
npx -y vui-registry-cli-v1@latest add interactive-3d-globeSource Code
"use client"
import createGlobe from "cobe"
import { useEffect, useRef, useState } from "react"
import { motion } from "framer-motion"
import { MapPin, 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: Record<string, [number, number]> = {
"Americas": [40, -100],
"Europe": [50, 10],
"Asia": [30, 100],
"Africa": [10, 20],
"India": [20, 77],
}
const handleRegionChange = (name: string, coords: [number, number]) => {
setActiveRegion(name)
setIsAutoRotating(false)
isAutoRotatingRef.current = false
const target = (coords[1] * Math.PI) / 180
targetPhiRef.current = 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 },
{ location: [40.7128, -74.006], size: 0.05 },
{ location: [51.5074, -0.1278], size: 0.05 },
{ location: [35.6762, 139.6503], size: 0.05 },
{ location: [28.6139, 77.2090], size: 0.05 },
{ location: [-33.8688, 151.2093], size: 0.05 },
{ location: [30.0444, 31.2357], size: 0.05 },
{ location: [55.7558, 37.6173], size: 0.05 },
{ location: [-23.5505, -46.6333], size: 0.05 },
],
onRender: (state) => {
if (!pointerInteracting.current) {
if (isAutoRotatingRef.current) {
currentPhi += 0.003
targetPhiRef.current = currentPhi
} else {
const dist = targetPhiRef.current - currentPhi
currentPhi += dist * 0.08
}
}
state.phi = currentPhi + rRef.current
state.width = width * 2
state.height = width * 2
},
})
setTimeout(() => {
if (canvasRef.current) canvasRef.current.style.opacity = "1"
})
return () => globe.destroy()
}, [])
return (
<div className="relative flex h-[600px] w-full flex-col items-center justify-center overflow-hidden bg-background font-sans">
<div
className="absolute inset-0 pointer-events-none"
style={{
background:
"radial-gradient(ellipse at center, hsl(var(--accent)/0.08) 0%, hsl(var(--foreground)/0.03) 45%, transparent 75%)",
}}
/>
<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
if (canvasRef.current) canvasRef.current.style.cursor = "grabbing"
setIsAutoRotating(false)
isAutoRotatingRef.current = false
}}
onPointerUp={() => {
pointerInteracting.current = null
if (canvasRef.current) canvasRef.current.style.cursor = "grab"
}}
onPointerOut={() => {
pointerInteracting.current = null
if (canvasRef.current) 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>
<div className="absolute bottom-8 z-10 w-full px-4">
<motion.div
initial={{ y: 30, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ type: "spring", damping: 22, stiffness: 110, delay: 0.1 }}
className="mx-auto flex max-w-fit items-center justify-center gap-1 rounded-full border border-foreground/5 bg-background/50 p-1 backdrop-blur-md"
>
{Object.entries(regions).map(([name, coords]) => (
<button
key={name}
onClick={() => handleRegionChange(name, coords)}
className={cn(
"group relative flex items-center gap-1.5 rounded-full px-3 sm:px-4 py-1.5 text-[11px] font-medium transition-all duration-300",
activeRegion === name
? "bg-foreground text-background"
: "text-muted-foreground hover:text-foreground"
)}
>
<MapPin
className={cn(
"h-3 w-3 transition-colors",
activeRegion === name
? "text-background/80"
: "text-muted-foreground group-hover:text-foreground"
)}
/>
{name}
</button>
))}
<div className="mx-1 h-4 w-px bg-foreground/10" />
<button
onClick={() => {
const newVal = !isAutoRotating
setIsAutoRotating(newVal)
isAutoRotatingRef.current = newVal
if (newVal) {
rRef.current = 0
pointerInteractionMovement.current = 0
}
}}
className={cn(
"group relative flex items-center justify-center rounded-full p-1.5 transition-all duration-300",
isAutoRotating
? "text-foreground"
: "text-muted-foreground hover:text-foreground"
)}
title="Auto Rotate"
>
<RotateCw
className={cn("h-3.5 w-3.5", isAutoRotating && "animate-spin")}
style={{ animationDuration: "6s" }}
/>
</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.