Globe
React + Tailwind
const css = `
@keyframes loader-globe-spin { to { transform: rotateX(-18deg) rotateY(360deg); } }
@keyframes loader-globe-orbit { to { transform: rotateZ(-20deg) rotateX(72deg) rotateZ(360deg); } }
.loader-globe {
position: relative;
width: 112px;
height: 112px;
perspective: 400px;
}
.loader-globe__sphere,
.loader-globe__orbit {
position: absolute;
inset: 16px;
transform-style: preserve-3d;
}
.loader-globe__sphere {
/* Same function list as the keyframe, or the 360deg turn interpolates as a no-op matrix. */
transform: rotateX(-18deg) rotateY(0deg);
animation: loader-globe-spin 7s linear infinite;
}
.loader-globe__ring {
position: absolute;
inset: 0;
border: 1px solid light-dark(#6d7179, #7d8087);
border-radius: 50%;
}
.loader-globe__ring--lat { border-color: light-dark(#cbced3, #393c41); }
.loader-globe__ring--equator { border-color: light-dark(#50535b, #c6c8cd); }
.loader-globe__orbit {
inset: 2px;
border-radius: 50%;
border: 1px dashed light-dark(#dbdde1, #2e3035);
animation: loader-globe-orbit 3.2s linear infinite;
transform: rotateZ(-20deg) rotateX(72deg) rotateZ(0deg);
}
.loader-globe__sat {
position: absolute;
left: 50%;
top: -3px;
width: 6px;
height: 6px;
margin-left: -3px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 8px light-dark(#373a40, #f1f2f3);
}
@media (prefers-reduced-motion: reduce) {
.loader-globe__sphere, .loader-globe__orbit { animation-duration: 30s; }
}
`;
const MERIDIANS = [0, 30, 60, 90, 120, 150];
const LATITUDES = [-55, -28, 28, 55];
export function GlobeLoader() {
return (
<>
<style>
{css}
</style>
<div className="loader-globe" role="status" aria-label="Loading">
<span className="loader-globe__sphere" aria-hidden="true">
{MERIDIANS.map((a) => (
<span key={a} className="loader-globe__ring" style={{ transform: `rotateY(${a}deg)` }} />
))}
<span className="loader-globe__ring loader-globe__ring--equator" style={{ transform: 'rotateX(90deg)' }} />
{LATITUDES.map((lat) => {
// A latitude circle: radius R·cos(lat), lifted R·sin(lat) along the axis.
const k = Math.cos((lat * Math.PI) / 180);
const lift = -40 * Math.sin((lat * Math.PI) / 180);
return (
<span
key={lat}
className="loader-globe__ring loader-globe__ring--lat"
style={{ transform: `translateY(${lift.toFixed(2)}px) rotateX(90deg) scale(${k.toFixed(3)})` }}
/>
);
})}
</span>
<span className="loader-globe__orbit" aria-hidden="true">
<span className="loader-globe__sat" />
</span>
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet. No JavaScript.
HTML
<div class="loader-globe" role="status" aria-label="Loading">
<span class="loader-globe__sphere" aria-hidden="true">
<span class="loader-globe__ring" style="transform:rotateY(0deg)"></span>
<span class="loader-globe__ring" style="transform:rotateY(30deg)"></span>
<span class="loader-globe__ring" style="transform:rotateY(60deg)"></span>
<span class="loader-globe__ring" style="transform:rotateY(90deg)"></span>
<span class="loader-globe__ring" style="transform:rotateY(120deg)"></span>
<span class="loader-globe__ring" style="transform:rotateY(150deg)"></span>
<span class="loader-globe__ring loader-globe__ring--equator" style="transform:rotateX(90deg)"></span>
<span class="loader-globe__ring loader-globe__ring--lat" style="transform:translateY(32.77px) rotateX(90deg) scale(0.574)"></span>
<span class="loader-globe__ring loader-globe__ring--lat" style="transform:translateY(18.78px) rotateX(90deg) scale(0.883)"></span>
<span class="loader-globe__ring loader-globe__ring--lat" style="transform:translateY(-18.78px) rotateX(90deg) scale(0.883)"></span>
<span class="loader-globe__ring loader-globe__ring--lat" style="transform:translateY(-32.77px) rotateX(90deg) scale(0.574)"></span>
</span>
<span class="loader-globe__orbit" aria-hidden="true">
<span class="loader-globe__sat"></span>
</span>
</div>CSS
@keyframes loader-globe-spin { to { transform: rotateX(-18deg) rotateY(360deg); } }
@keyframes loader-globe-orbit { to { transform: rotateZ(-20deg) rotateX(72deg) rotateZ(360deg); } }
.loader-globe {
position: relative;
width: 112px;
height: 112px;
perspective: 400px;
}
.loader-globe__sphere,
.loader-globe__orbit {
position: absolute;
inset: 16px;
transform-style: preserve-3d;
}
.loader-globe__sphere {
/* Same function list as the keyframe, or the 360deg turn interpolates as a no-op matrix. */
transform: rotateX(-18deg) rotateY(0deg);
animation: loader-globe-spin 7s linear infinite;
}
.loader-globe__ring {
position: absolute;
inset: 0;
border: 1px solid light-dark(#6d7179, #7d8087);
border-radius: 50%;
}
.loader-globe__ring--lat { border-color: light-dark(#cbced3, #393c41); }
.loader-globe__ring--equator { border-color: light-dark(#50535b, #c6c8cd); }
.loader-globe__orbit {
inset: 2px;
border-radius: 50%;
border: 1px dashed light-dark(#dbdde1, #2e3035);
animation: loader-globe-orbit 3.2s linear infinite;
transform: rotateZ(-20deg) rotateX(72deg) rotateZ(0deg);
}
.loader-globe__sat {
position: absolute;
left: 50%;
top: -3px;
width: 6px;
height: 6px;
margin-left: -3px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 8px light-dark(#373a40, #f1f2f3);
}
@media (prefers-reduced-motion: reduce) {
.loader-globe__sphere, .loader-globe__orbit { animation-duration: 30s; }
}Goal: Create a "Globe" loader.
Visual behavior: A wireframe planet of meridians and latitude rings spinning in 3D, with a satellite on a tilted orbit circling it.
Technique:
- Six bordered circles rotated round Y by 0 to 150deg make the meridians; an equator is rotated 90deg on X.
- Each latitude is a ring rotated flat, scaled by cos(lat) and lifted by sin(lat) along the axis, so it sits exactly on the sphere.
- The sphere (preserve-3d) spins on Y with a slight forward tilt; the base transform repeats the keyframe's function list, or the 360deg turn would interpolate as a no-op.
- A dashed orbit ring is tilted and spins faster, carrying a glowing satellite.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the spin slows to a 30s period.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Globe" loader.
Visual behavior: A wireframe planet of meridians and latitude rings spinning in 3D, with a satellite on a tilted orbit circling it.
Technique:
- Six bordered circles rotated round Y by 0 to 150deg make the meridians; an equator is rotated 90deg on X.
- Each latitude is a ring rotated flat, scaled by cos(lat) and lifted by sin(lat) along the axis, so it sits exactly on the sphere.
- The sphere (preserve-3d) spins on Y with a slight forward tilt; the base transform repeats the keyframe's function list, or the 360deg turn would interpolate as a no-op.
- A dashed orbit ring is tilted and spins faster, carrying a glowing satellite.
Accessibility: The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the spin slows to a 30s period.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A wireframe planet of meridians and latitude rings spinning in 3D, with a satellite on a tilted orbit circling it.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- continuous, loop, 3d
- Techniques
- preserve 3d, 3d transform, keyframes
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the spin slows to a 30s period.