Free Webflow components / Effect: interactive dot globe
Webflow interactive dot globe
A free spinning dot globe for Webflow that visitors can drag to turn. Copy it, press ⌘V in the Designer, and change the colors and text around it.
- JavaScript
- Responsive
What you receive in Webflow
- 11 native elements and 8 classes.
- Breakpoints: mobile landscape (767px), tablet (991px).
- States: active.
- 2 Embed elements keep the CSS or script that Webflow cannot express. To edit an Embed, you must have a paid Webflow site plan.
How to paste it
- Click Copy for Webflow.
- Open your page in the Webflow Designer.
- Press ⌘V or Ctrl+V.
HTML
<section class="gl">
<div class="gl-inner">
<div class="gl-copy">
<p class="gl-eyebrow">Global</p>
<h2 class="gl-title">Teams in 40 countries ship with us</h2>
<p class="gl-text">A globe of dots with depth: near dots are larger and brighter. It turns on its own; drag to spin it.</p>
</div>
<div class="gl-stage" role="img" aria-label="Rotating globe of dots"><div class="gl-fallback" aria-hidden="true"></div></div>
</div>
</section>
<style>
.gl { font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif; background: #020617; color: #e2e8f0; padding: 112px 24px; overflow: hidden; }
.gl-inner { max-width: 1120px; margin: 0 auto; display: flex; align-items: center; gap: 48px; }
.gl-copy { flex: 1; }
.gl-eyebrow { font-size: 13px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: #38bdf8; margin: 0 0 16px; }
.gl-title { font-size: 48px; line-height: 1.08; letter-spacing: -0.03em; color: #f8fafc; margin: 0 0 16px; }
.gl-text { font-size: 17px; line-height: 1.6; color: #94a3b8; margin: 0; max-width: 420px; }
.gl-stage { position: relative; flex: 0 0 520px; height: 520px; cursor: grab; touch-action: pan-y; }
.gl-stage:active { cursor: grabbing; }
.gl-fallback { position: absolute; top: 10%; right: 10%; bottom: 10%; left: 10%; border-radius: 50%; background-image: radial-gradient(circle at 35% 30%, rgba(56, 189, 248, 0.55) 0%, rgba(14, 116, 144, 0.35) 35%, rgba(2, 6, 23, 0) 70%); box-shadow: 0 0 120px rgba(56, 189, 248, 0.25); }
.gl-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
@media (max-width: 991px) { .gl-inner { flex-direction: column; align-items: stretch; } .gl-stage { flex-basis: auto; width: 100%; max-width: 520px; height: auto; aspect-ratio: 1 / 1; margin: 0 auto; } }
@media (max-width: 767px) { .gl { padding: 80px 20px; } .gl-title { font-size: 34px; } }
</style>
<script>
(function () {
var reduce = window.matchMedia && matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".gl-stage").forEach(function (stage) {
var cv = document.createElement("canvas"); cv.className = "gl-canvas";
var ctx = cv.getContext("2d"); if (!ctx) return;
stage.appendChild(cv);
var fb = stage.querySelector(".gl-fallback"); if (fb) fb.style.opacity = "0.35";
// points spread evenly on a sphere; "land" is a cheap pattern of blobs so the globe has continents
var N = 1400, pts = [], g = Math.PI * (3 - Math.sqrt(5));
for (var i = 0; i < N; i++) {
var y = 1 - (i / (N - 1)) * 2, rad = Math.sqrt(1 - y * y), th = g * i;
var x = Math.cos(th) * rad, z = Math.sin(th) * rad;
var lat = Math.asin(y), lon = Math.atan2(z, x);
var land = Math.sin(lon * 2.1 + 0.6) * Math.cos(lat * 3.2) + Math.sin(lon * 4.7 - lat * 2.3) * 0.55 > 0.25;
pts.push([x, y, z, land]);
}
var rotY = 0.6, rotX = -0.35, vel = 0.0035, drag = null, visible = true;
function size() {
var dpr = Math.min(2, window.devicePixelRatio || 1), w = Math.round(stage.clientWidth * dpr), h = Math.round(stage.clientHeight * dpr);
if (w && h && (w !== cv.width || h !== cv.height)) { cv.width = w; cv.height = h; }
}
// measure again whenever the stage changes size (layout settling, breakpoints, a resized frame)
size(); window.addEventListener("resize", size);
if ("ResizeObserver" in window) new ResizeObserver(size).observe(stage);
// the pointer is over the globe, so it is on screen whatever the observer last said
stage.addEventListener("pointerenter", function () { visible = true; });
stage.addEventListener("pointerdown", function (e) { visible = true; drag = { x: e.clientX, y: e.clientY, ry: rotY, rx: rotX }; stage.setPointerCapture(e.pointerId); });
stage.addEventListener("pointermove", function (e) { if (!drag) return; rotY = drag.ry + (e.clientX - drag.x) * 0.008; rotX = Math.max(-1.2, Math.min(1.2, drag.rx + (e.clientY - drag.y) * 0.006)); });
["pointerup", "pointercancel"].forEach(function (t) { stage.addEventListener(t, function () { drag = null; }); });
// pause only after the globe has been seen and then scrolled away, so a late or missing first report never leaves it blank
var seen = false;
if ("IntersectionObserver" in window) new IntersectionObserver(function (es) { if (es[0].isIntersecting) seen = true; visible = es[0].isIntersecting || !seen; }).observe(stage);
function frame() {
requestAnimationFrame(frame);
if (!visible) return;
if (!cv.width || !cv.height) { size(); if (!cv.width) return; }
if (!drag && !reduce) rotY += vel;
var w = cv.width, h = cv.height, R = Math.min(w, h) * 0.42, cx = w / 2, cy = h / 2;
ctx.clearRect(0, 0, w, h);
var sy = Math.sin(rotY), cyy = Math.cos(rotY), sx = Math.sin(rotX), cxx = Math.cos(rotX);
var proj = [];
for (var i = 0; i < N; i++) {
var p = pts[i], x1 = p[0] * cyy + p[2] * sy, z1 = -p[0] * sy + p[2] * cyy;
var y2 = p[1] * cxx - z1 * sx, z2 = p[1] * sx + z1 * cxx;
proj.push([cx + x1 * R, cy - y2 * R, z2, p[3]]);
}
proj.sort(function (a, b) { return a[2] - b[2]; });
var dpr = cv.width / Math.max(1, stage.clientWidth);
for (var j = 0; j < N; j++) {
var q = proj[j], depth = (q[2] + 1) / 2; // 0 = back, 1 = front
var r0 = (q[3] ? 2.1 : 1.2) * dpr * (0.45 + depth * 0.85);
var a = q[3] ? 0.18 + depth * 0.82 : 0.06 + depth * 0.3;
ctx.fillStyle = q[3] ? "rgba(56,189,248," + a + ")" : "rgba(148,163,184," + a + ")";
ctx.beginPath(); ctx.arc(q[0], q[1], r0, 0, 6.2832); ctx.fill();
}
}
requestAnimationFrame(frame);
});
})();
</script>
More examples
Questions
Is it WebGL?
No. It's drawn on a 2D canvas, so it works in every browser without WebGL.
Can I change the dot color?
Yes. The dot colors are in the script in the custom code Embed (look for rgba(56,189,248,…)). Editing an Embed needs a paid site plan.
Is this effect component free?
Yes. Copy it with no account and use it on client sites and your own sites.
Does it work on the free Webflow plan?
Yes. It pastes on every Webflow plan, including Starter. If it includes an Embed element, editing that Embed's code needs a paid site plan.