Components
A 3D animated Shopify-style globe built with react-globe.gl, featuring teal-green continents, golden arcs representing connections, and interactive pin drops. Ideal for dashboards, landing pages, or data visualizations.
Loading preview...
"use client";
import * as React from "react";
import { useRef, useState, useEffect, Suspense } from "react";
// Dynamically import the 3D globe only on the client.
const Globe = React.lazy(() => import("react-globe.gl"));
export default function ShopifyStyleGlobe() {
const globeRef = useRef<any>(null);
const [mounted, setMounted] = useState(false);
const [pins, setPins] = useState<any[]>([]);
const [arcs, setArcs] = useState<any[]>([]);
useEffect(() => setMounted(true), []);
// Texture and background images
const globeImageUrl = "https://unpkg.com/three-globe/example/img/earth-dark.jpg";
const bumpImageUrl = "https://unpkg.com/three-globe/example/img/earth-topology.png";
const backgroundImageUrl = "https://unpkg.com/three-globe/example/img/night-sky.png";
// Sample city coordinates
const cities = [
[45.5017, -73.5673],
[48.8566, 2.3522],
[6.5244, 3.3792],
[35.6762, 139.6503],
[-33.8688, 151.2093],
[-23.5505, -46.6333],
];
// Build animated arcs (simulating orders) on mount
useEffect(() => {
const newArcs = Array.from({ length: 20 }, () => {
const from = cities[Math.floor(Math.random() * cities.length)];
const to = cities[Math.floor(Math.random() * cities.length)];
return {
startLat: from[0],
startLng: from[1],
endLat: to[0],
endLng: to[1],
// white‑to‑gold gradient similar to Shopify’s trails
color: ["#ffffff", "#ffd700"],
};
});
setArcs(newArcs);
}, []);
// Click to drop a pin
const handleGlobeClick = ({ lat, lng }: { lat: number; lng: number }) => {
setPins((prev) => [
...prev,
{
lat,
lng,
size: 0.3,
color: "#ffbf00",
label: `📍 ${lat.toFixed(1)}, ${lng.toFixed(1)}`,
},
]);
};
// Focus camera on a pin
const handleLabelClick = (p: any) => {
const g = globeRef.current;
if (g) {
g.pointOfView({ lat: p.lat, lng: p.lng, altitude: 1.5 }, 1500);
}
};
return (
<div className="w-full h-[600px] bg-black relative">
{mounted && (
<Suspense fallback={<div className="text-white/70 text-sm p-4">Loading Globe…</div>}>
<Globe
ref={globeRef}
globeImageUrl={globeImageUrl}
bumpImageUrl={bumpImageUrl}
backgroundImageUrl={backgroundImageUrl}
showAtmosphere
atmosphereColor="#94650d" // warm glow around Earth
atmosphereAltitude={0.35}
// Arc configuration inspired by Shopify’s curved order trails
arcsData={arcs}
arcColor={(d) => d.color}
arcAltitude={0.2} // raise arcs off the globe
arcStroke={0.5} // thicker, smoother trails
arcDashLength={0.7} // longer dashes for continuous feel
arcDashGap={0.3}
arcDashInitialGap={() => Math.random()} // staggered trails
arcDashAnimateTime={3500} // slower, more graceful animation
arcAltitudeAutoScale={false}
// Rotate the globe slowly for continuous motion
autoRotate={true}
autoRotateSpeed={0.5}
// Initial camera view
pointOfView={{ lat: 25, lng: -30, altitude: 2.2 }}
// Pin (label) configuration
labelsData={pins}
labelText={(d) => d.label}
labelSize={1.5}
labelDotRadius={0.5}
labelColor={(d) => d.color}
onLabelClick={handleLabelClick}
onGlobeClick={handleGlobeClick}
enablePointerInteraction
/>
</Suspense>
)}
<div className="absolute bottom-3 left-4 text-xs text-white/70 flex items-center gap-1">
</div>
</div>
);
}