An interactive 3D globe component built with Three.js and React, allowing users to explore geographic data in a visually engaging way. Features include customizable markers, smooth animations, and responsive design for seamless integration into any web project.
ScrollInteractiveThree.tsx (Three.js Globe Core)
"use client";
import { useEffect, useRef } from "react";
import type { ReactNode } from "react";
import * as THREE from "three";
import { isWebGLAvailable, useWebGLAvailable } from "./isWebGLAvailable";
type ScrollInteractiveThreeProps = {
/** Rendered in place of the 3D scene when WebGL is unavailable. */
fallback?: ReactNode;
};
const ScrollInteractiveThree = ({ fallback }: ScrollInteractiveThreeProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const webglAvailable = useWebGLAvailable();
useEffect(() => {
if (typeof window === "undefined" || !containerRef.current) return;
let animationFrameId: number;
const container = containerRef.current;
const width = container.clientWidth || 400;
const height = container.clientHeight || 400;
// 1. Scene setup
const scene = new THREE.Scene();
// 2. Camera setup
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 100);
camera.position.z = 6;
// 3. Renderer setup (alpha: true for transparent background)
// WebGL can be unavailable (no GPU, hardware acceleration disabled, restrictive
// drivers). Probe first so we skip the renderer entirely — and avoid Three's own
// console.error — degrading gracefully so the rest of the page still renders.
if (!isWebGLAvailable()) {
console.warn("ScrollInteractiveThree: WebGL unavailable, skipping 3D scene.");
return;
}
let renderer: THREE.WebGLRenderer;
try {
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
} catch (error) {
console.warn(
"ScrollInteractiveThree: WebGL renderer creation failed, skipping 3D scene.",
error,
);
return;
}
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // Cap at 2 for performance
container.appendChild(renderer.domElement);
// 4. Create Group to hold all meshes
const mainGroup = new THREE.Group();
const coreGroup = new THREE.Group();
scene.add(mainGroup);
scene.add(coreGroup);
// 5. Geometries
const outerGeo = new THREE.SphereGeometry(2, 16, 16);
const innerGeo = new THREE.IcosahedronGeometry(1.2, 1);
// 6. Materials & Meshes
// Outer Node points
const pointsMat = new THREE.PointsMaterial({
color: 0x00d4ff, // HUD Cyan
size: 0.08,
transparent: true,
opacity: 0.75,
});
const outerPoints = new THREE.Points(outerGeo, pointsMat);
mainGroup.add(outerPoints);
// Outer wireframe connections
const wireframeMat = new THREE.LineBasicMaterial({
color: 0x00d4ff,
transparent: true,
opacity: 0.15,
});
const outerLines = new THREE.LineSegments(new THREE.WireframeGeometry(outerGeo), wireframeMat);
mainGroup.add(outerLines);
// Inner Core wireframe (Violet)
const coreMat = new THREE.LineBasicMaterial({
color: 0x8b5cf6, // Violet
transparent: true,
opacity: 0.35,
});
const coreLines = new THREE.LineSegments(new THREE.WireframeGeometry(innerGeo), coreMat);
coreGroup.add(coreLines);
// Inner Core points
const corePointsMat = new THREE.PointsMaterial({
color: 0x8b5cf6,
size: 0.06,
transparent: true,
opacity: 0.6,
});
const corePoints = new THREE.Points(innerGeo, corePointsMat);
coreGroup.add(corePoints);
// 7. Scroll Listener & Lerp Variables
let targetRotationX = 0;
let targetRotationZ = 0;
const handleScroll = () => {
const scrollTop = window.scrollY;
targetRotationX = scrollTop * 0.0012;
targetRotationZ = scrollTop * 0.0004;
};
window.addEventListener("scroll", handleScroll, { passive: true });
// 8. Intersection Observer to pause when offscreen
let isIntersecting = true;
const observer = new IntersectionObserver(
([entry]) => {
isIntersecting = entry.isIntersecting;
},
{ threshold: 0.05 },
);
observer.observe(container);
// 9. Resize Handler
const handleResize = () => {
const w = container.clientWidth;
const h = container.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
};
window.addEventListener("resize", handleResize);
// 10. Animation Loop
const animate = () => {
animationFrameId = requestAnimationFrame(animate);
if (!isIntersecting) return;
// Constant subtle idle rotations
mainGroup.rotation.y += 0.002;
coreGroup.rotation.y -= 0.004; // Rotate inner core in opposite direction
// Smoothly lerp towards target scroll rotation
mainGroup.rotation.x += (targetRotationX - mainGroup.rotation.x) * 0.08;
mainGroup.rotation.z += (targetRotationZ - mainGroup.rotation.z) * 0.08;
coreGroup.rotation.x -= (targetRotationX - coreGroup.rotation.x) * 0.06;
coreGroup.rotation.z -= (targetRotationZ - coreGroup.rotation.z) * 0.06;
renderer.render(scene, camera);
};
// Start loop
animate();
// Cleanup
return () => {
cancelAnimationFrame(animationFrameId);
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", handleResize);
observer.disconnect();
if (container.contains(renderer.domElement)) {
container.removeChild(renderer.domElement);
}
outerGeo.dispose();
innerGeo.dispose();
pointsMat.dispose();
wireframeMat.dispose();
coreMat.dispose();
corePointsMat.dispose();
renderer.dispose();
};
}, []);
if (!webglAvailable && fallback) return <>{fallback}</>;
return (
<div
ref={containerRef}
style={{
width: "100%",
height: "100%",
minHeight: "350px",
position: "relative",
}}
/>
);
};
export default ScrollInteractiveThree;