Skip to content
Neon UI
Esc
navigateopen⌘Jpreview
On this page

RegionGlobe

Tilted 3D dotted globe that spins to the selected server region.

RegionGlobe is the ornamental half of region picking: a Three.js globe (via globe.gl) with three land treatments via variant: relief (default — the Natural Earth topology map as both grayscale texture and height map, tinted by muted-foreground so it stays monochrome in either theme), dots (the house dot-matrix land bitmap on a plain sphere), and outlines (Natural Earth country borders). The atmosphere glow is tunable too: glow={false} removes it, glowColor/glowAltitude restyle it. Changing regions emits a travel effect after globe.gl’s emit-arcs example: a dashed primary arc flies from the previous region while rings ripple out of both endpoints. Region markers are clickable dots with a hover popover (name, mono region id, and a color-coded ping: x ms when latencies is provided); while spin is on the globe idles on a slow auto-rotate, and choosing a region flies the camera to it on a downward tilt. Colors are probed live from the theme tokens, and under prefers-reduced-motion both the spin and the flight are disabled. Pair it with RegionSelect as the input and useRegionPing for a live latency caption.

ping: 92 ms

Install

npx shadcn@latest add https://ui.neon.com/r/region-globe.json

Usage

import { RegionGlobe } from "@/components/neon-ui/region-globe";
import { useRegionPing } from "@/hooks/use-region-ping";

const { pings } = useRegionPing({
  "aws-us-east-1": "https://us-east-1.aws.example.com/health",
});

<RegionGlobe regions={regions} value={region} />
<p>~{pings[region]} ms from your location</p>

Wiring to Neon

The globe takes the same ServerRegion[] as RegionSelect, built on the server from the active regions:

import { createNeonClient } from "@/lib/neon-client";

const neon = createNeonClient(process.env.NEON_API_KEY);
const { data } = await neon.regions.list();

const regions = (data ?? [])
  .filter((region) => region.geo_lat && region.geo_long)
  .map((region) => ({
    id: region.region_id,
    name: region.name,
    provider: region.region_id.split("-")[0].toUpperCase(),
    lat: Number(region.geo_lat),
    lng: Number(region.geo_long),
  }));

Latency is measured in the browser, not fetched: pass the regions to useRegionPing in a client component and hand the result back as latencies. The hook is its own registry item, so anything that needs a ping can install it without the globe.

Props

PropType
regions?ServerRegion[]

Regions rendered as markers on the globe.

TypeServerRegion[]
value?string

Region id the globe spins to; its marker renders larger.

Typestring
onValueChange?(value: string) => void

Makes the marker dots clickable; called with the picked region id.

Type(value: string) => void
latencies?Record<string, number | null>

Measured round-trips by region id; shown color-coded in each marker's hover popover.

TypeRecord<string, number | null>
spin?boolean

Keep the globe slowly turning, e.g. until the user picks a region.

Typeboolean
Defaultfalse
tilt?number

Camera latitude lead in degrees; a slight downward tilt.

Typenumber
Default12
variant?"relief" | "dots" | "outlines"

Land treatment: monochrome height map, the house dot-matrix bitmap, or country borders. Fixed at mount — remount (key) to switch.

Type"relief" | "dots" | "outlines"
Default"relief"
glow?boolean

Set false to remove the atmosphere glow entirely.

Typeboolean
Defaulttrue
glowColor?string

Glow color; defaults to the theme's primary token.

Typestring
glowAltitude?number

Glow reach as a fraction of the globe radius.

Typenumber
Default0.12
countriesUrl?string

Country borders GeoJSON for the outlines variant; defaults to globe.gl's Natural Earth 110m dataset (CDN).

Typestring
textureUrl?string

Grayscale surface texture; defaults to the Natural Earth topology map (CDN, relief variant only).

Typestring
bumpUrl?string

Height map for the relief; defaults to the same topology map.

Typestring

useRegionPing

Install separately (https://ui.neon.com/r/use-region-ping.json) or let a block pull it in. The hook measures browser round-trips with opaque no-cors HEAD requests — a warmup pays DNS + TLS, then the best of samples sequential timings wins. Endpoints just need to be reachable; the response is never read.

PropType
urls?Record<string, string>

Endpoint per region id to measure against.

TypeRecord<string, string>
samples?number

Timed requests per region after the warmup.

Typenumber
Default3
enabled?boolean

Set false to defer, then call refresh() when ready.

Typeboolean
Defaulttrue

Returns { pings, status, refresh }pings maps region ids to milliseconds (or null when unreachable), status walks idle → measuring → done.

Accessibility

  • The globe is decorative (aria-hidden canvas); dot clicks are a pointer-only enhancement, so selection state must live in a real input like RegionSelect.
  • Idle spin, camera flight, and the arc/ring travel effect are disabled under prefers-reduced-motion — the globe jumps to the region instead.
  • The default texture loads from the three-globe CDN; pass textureUrl/bumpUrl to self-host.
  • Note the size trade-off: globe.gl brings Three.js (~160 kB gzip); use RegionSelect’s flat map where that’s too heavy.
  • Caption ping readouts should be aria-live="polite", as in the demo.