Map Region Select
Original · freeclickable region selector map
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/map-region-select.json"use client";
import { useId, useRef, useState } from "react";
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type GridKey = "n" | "s" | "e" | "f" | "m" | "a";
interface Region {
id: GridKey;
name: string;
hub: string;
hubCell: [number, number];
code: string;
pops: number;
latency: number;
rate: number;
capacity: string;
status: "Live" | "Expanding";
cities: string[];
dot: string;
idle: string;
active: string;
hubFill: string;
pulse: string;
chipOn: string;
ring: string;
}
const REGIONS: Region[] = [
{
id: "n",
name: "North America",
hub: "Ashburn",
hubCell: [7, 4],
code: "na-east-1",
pops: 14,
latency: 11,
rate: 0.085,
capacity: "3.8 Tbps",
status: "Live",
cities: ["Dallas", "San Jose", "Chicago", "Toronto", "Seattle", "Montréal"],
dot: "bg-indigo-500",
idle: "fill-indigo-500/20 dark:fill-indigo-400/20",
active: "fill-indigo-500/85 dark:fill-indigo-400/80",
hubFill: "fill-indigo-600 dark:fill-indigo-400",
pulse: "fill-indigo-500/40",
chipOn: "border-indigo-500/60 bg-indigo-500/10 text-indigo-700 dark:text-indigo-300",
ring: "focus-visible:ring-indigo-500",
},
{
id: "s",
name: "South America",
hub: "São Paulo",
hubCell: [9, 10],
code: "sa-east-1",
pops: 5,
latency: 24,
rate: 0.14,
capacity: "640 Gbps",
status: "Expanding",
cities: ["Rio de Janeiro", "Buenos Aires", "Santiago", "Bogotá"],
dot: "bg-emerald-500",
idle: "fill-emerald-500/20 dark:fill-emerald-400/20",
active: "fill-emerald-500/85 dark:fill-emerald-400/80",
hubFill: "fill-emerald-600 dark:fill-emerald-400",
pulse: "fill-emerald-500/40",
chipOn: "border-emerald-500/60 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
ring: "focus-visible:ring-emerald-500",
},
{
id: "e",
name: "Europe",
hub: "Frankfurt",
hubCell: [13, 3],
code: "eu-central-1",
pops: 17,
latency: 9,
rate: 0.08,
capacity: "4.2 Tbps",
status: "Live",
cities: ["Amsterdam", "London", "Paris", "Stockholm", "Madrid", "Warsaw", "Milan"],
dot: "bg-sky-500",
idle: "fill-sky-500/20 dark:fill-sky-400/20",
active: "fill-sky-500/85 dark:fill-sky-400/80",
hubFill: "fill-sky-600 dark:fill-sky-400",
pulse: "fill-sky-500/40",
chipOn: "border-sky-500/60 bg-sky-500/10 text-sky-700 dark:text-sky-300",
ring: "focus-visible:ring-sky-500",
},
{
id: "f",
name: "Africa",
hub: "Johannesburg",
hubCell: [13, 11],
code: "af-south-1",
pops: 4,
latency: 31,
rate: 0.155,
capacity: "410 Gbps",
status: "Expanding",
cities: ["Lagos", "Nairobi", "Cape Town"],
dot: "bg-amber-500",
idle: "fill-amber-500/25 dark:fill-amber-400/20",
active: "fill-amber-500/85 dark:fill-amber-400/80",
hubFill: "fill-amber-600 dark:fill-amber-400",
pulse: "fill-amber-500/40",
chipOn: "border-amber-500/60 bg-amber-500/10 text-amber-700 dark:text-amber-300",
ring: "focus-visible:ring-amber-500",
},
{
id: "m",
name: "Middle East & India",
hub: "Mumbai",
hubCell: [16, 5],
code: "me-south-1",
pops: 6,
latency: 18,
rate: 0.125,
capacity: "900 Gbps",
status: "Live",
cities: ["Dubai", "Chennai", "Delhi", "Tel Aviv", "Riyadh"],
dot: "bg-rose-500",
idle: "fill-rose-500/20 dark:fill-rose-400/20",
active: "fill-rose-500/85 dark:fill-rose-400/80",
hubFill: "fill-rose-600 dark:fill-rose-400",
pulse: "fill-rose-500/40",
chipOn: "border-rose-500/60 bg-rose-500/10 text-rose-700 dark:text-rose-300",
ring: "focus-visible:ring-rose-500",
},
{
id: "a",
name: "Asia Pacific",
hub: "Singapore",
hubCell: [21, 8],
code: "ap-southeast-1",
pops: 12,
latency: 14,
rate: 0.11,
capacity: "2.6 Tbps",
status: "Live",
cities: ["Tokyo", "Seoul", "Sydney", "Hong Kong", "Osaka", "Jakarta"],
dot: "bg-violet-500",
idle: "fill-violet-500/20 dark:fill-violet-400/20",
active: "fill-violet-500/85 dark:fill-violet-400/80",
hubFill: "fill-violet-600 dark:fill-violet-400",
pulse: "fill-violet-500/40",
chipOn: "border-violet-500/60 bg-violet-500/10 text-violet-700 dark:text-violet-300",
ring: "focus-visible:ring-violet-500",
},
];
const GRID: string[] = [
"..nnnnnnnn..eeeaaaaaaaaa..",
".nnnnnnnn...eeeaaaaaaaaaa.",
"..nnnnnnn...eeeeaaaaaaaaa.",
"..nnnnnn...eeee..aaaaaaaa.",
"...nnnnn....eemm..aaaaaa..",
"....nnn.....fffmm...aaaa..",
".....nn....ffffmm...aaa...",
".......ss..ffffm....aaa...",
".......sss..fff......aa...",
".......sss..fff.......a...",
".......sss..fff.....aaa...",
".......ss...ff......aaa...",
".......s............aaa...",
".......s.............a....",
];
const HEX_R = 10;
const HEX_W = Math.sqrt(3) * HEX_R;
const HEX_V = 1.5 * HEX_R;
const HEX_POINTS = "0,-10 -8.66,-5 -8.66,5 0,10 8.66,5 8.66,-5";
interface Cell {
x: number;
y: number;
}
function cellCenter(col: number, row: number): Cell {
return { x: col * HEX_W + (row % 2 === 1 ? HEX_W / 2 : 0), y: row * HEX_V };
}
const OCEAN: Cell[] = [];
const LAND: Record<GridKey, Cell[]> = { n: [], s: [], e: [], f: [], m: [], a: [] };
GRID.forEach((row, ri) => {
for (let ci = 0; ci < row.length; ci++) {
const ch = row.charAt(ci);
const c = cellCenter(ci, ri);
if (ch === ".") {
OCEAN.push(c);
continue;
}
LAND[ch as GridKey].push(c);
}
});
function waveDelay(c: Cell): string {
return `${Math.round(c.x * 0.62 + c.y * 1.35)}ms`;
}
export default function MapRegionSelect() {
const uid = useId();
const reduce = useReducedMotion();
const [selected, setSelected] = useState<GridKey>("e");
const [hovered, setHovered] = useState<GridKey | null>(null);
const [enabled, setEnabled] = useState<GridKey[]>(["e", "n"]);
const chipRefs = useRef<Record<string, HTMLButtonElement | null>>({});
const active = REGIONS.find((r) => r.id === selected) ?? REGIONS[0];
const on = REGIONS.filter((r) => enabled.includes(r.id));
const totalPops = on.reduce((sum, r) => sum + r.pops, 0);
const worstLatency = on.length ? Math.max(...on.map((r) => r.latency)) : 0;
const blended = on.length ? on.reduce((sum, r) => sum + r.rate, 0) / on.length : 0;
function focusChip(id: GridKey) {
setSelected(id);
chipRefs.current[id]?.focus();
}
function step(delta: number) {
const i = REGIONS.findIndex((r) => r.id === selected);
const next = REGIONS[(i + delta + REGIONS.length) % REGIONS.length];
focusChip(next.id);
}
function onChipKeyDown(event: ReactKeyboardEvent<HTMLDivElement>) {
switch (event.key) {
case "ArrowRight":
case "ArrowDown":
event.preventDefault();
step(1);
break;
case "ArrowLeft":
case "ArrowUp":
event.preventDefault();
step(-1);
break;
case "Home":
event.preventDefault();
focusChip(REGIONS[0].id);
break;
case "End":
event.preventDefault();
focusChip(REGIONS[REGIONS.length - 1].id);
break;
default:
break;
}
}
function toggle(id: GridKey) {
setEnabled((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
}
const activeOn = enabled.includes(active.id);
return (
<section className="relative w-full overflow-hidden bg-slate-50 py-20 dark:bg-slate-950 sm:py-28">
<style>{`
@keyframes mrs-tile-in {
from { opacity: 0; transform: translateY(7px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes mrs-hub-pulse {
0% { transform: scale(0.55); opacity: 0.55; }
70% { transform: scale(2.7); opacity: 0; }
100% { transform: scale(2.7); opacity: 0; }
}
.mrs-tile { animation: mrs-tile-in 620ms cubic-bezier(0.22, 1, 0.36, 1) both; }
.mrs-hub { animation: mrs-hub-pulse 2600ms ease-out infinite; transform-box: fill-box; transform-origin: center; }
@media (prefers-reduced-motion: reduce) {
.mrs-tile, .mrs-hub { animation: none !important; opacity: 1; }
.mrs-hub { opacity: 0.28; }
}
`}</style>
<div className="mx-auto w-full max-w-6xl px-6">
<div className="max-w-2xl">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-slate-500 dark:text-slate-400">
Edge network
</p>
<h2 className="mt-3 text-3xl font-semibold tracking-tight text-slate-900 dark:text-slate-50 sm:text-4xl">
Pick the regions your traffic lands in
</h2>
<p className="mt-4 text-base leading-relaxed text-slate-600 dark:text-slate-400">
58 points of presence across six regions. Select one to inspect its hub, capacity and
egress rate — then switch it on to add it to your routing plan.
</p>
</div>
<div
role="radiogroup"
aria-labelledby={`${uid}-rg`}
onKeyDown={onChipKeyDown}
className="mt-10 flex flex-wrap gap-2"
>
<span id={`${uid}-rg`} className="sr-only">
Choose an edge region to inspect
</span>
{REGIONS.map((r) => {
const isSel = selected === r.id;
return (
<button
key={r.id}
type="button"
role="radio"
aria-checked={isSel}
tabIndex={isSel ? 0 : -1}
ref={(el) => {
chipRefs.current[r.id] = el;
}}
onClick={() => setSelected(r.id)}
onMouseEnter={() => setHovered(r.id)}
onMouseLeave={() => setHovered(null)}
className={[
"inline-flex items-center gap-2 rounded-full border px-3.5 py-2 text-sm font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-950",
r.ring,
isSel
? r.chipOn
: "border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-900/50 dark:text-slate-400 dark:hover:border-slate-700 dark:hover:text-slate-100",
].join(" ")}
>
<span className={`h-2 w-2 shrink-0 rounded-full ${r.dot}`} aria-hidden="true" />
{r.name}
{enabled.includes(r.id) ? (
<span className="ml-0.5 rounded-full bg-slate-900/85 px-1.5 py-0.5 text-[10px] font-semibold leading-none text-white dark:bg-white/90 dark:text-slate-900">
ON
</span>
) : null}
</button>
);
})}
</div>
<div className="mt-6 grid gap-6 lg:grid-cols-[1.7fr_1fr]">
<div className="rounded-3xl border border-slate-200 bg-white p-4 shadow-sm dark:border-slate-800 dark:bg-slate-900/40 sm:p-6">
<svg
viewBox="-14 -16 478 236"
className="h-auto w-full select-none"
aria-hidden="true"
focusable="false"
>
<g>
{OCEAN.map((c, i) => (
<g key={`o-${i}`} transform={`translate(${c.x},${c.y})`}>
<g className="mrs-tile" style={{ animationDelay: waveDelay(c) }}>
<polygon
points={HEX_POINTS}
className="fill-slate-200/70 dark:fill-slate-800/70"
style={{
transform: "scale(0.74)",
transformBox: "fill-box",
transformOrigin: "center",
}}
/>
</g>
</g>
))}
</g>
{REGIONS.map((r) => {
const isSel = selected === r.id;
const isHot = hovered === r.id;
const isOn = enabled.includes(r.id);
const scale = isSel ? 0.99 : isHot ? 0.93 : 0.86;
const hub = cellCenter(r.hubCell[0], r.hubCell[1]);
const labelW = r.hub.length * 4.5 + 14;
return (
<g
key={r.id}
className="cursor-pointer"
onClick={() => setSelected(r.id)}
onMouseEnter={() => setHovered(r.id)}
onMouseLeave={() => setHovered(null)}
>
{LAND[r.id].map((c, i) => (
<g key={`${r.id}-${i}`} transform={`translate(${c.x},${c.y})`}>
<g className="mrs-tile" style={{ animationDelay: waveDelay(c) }}>
<polygon
points={HEX_POINTS}
className={`transition-transform duration-300 ease-out ${
isOn ? r.active : r.idle
}`}
style={{
transform: `scale(${scale})`,
transformBox: "fill-box",
transformOrigin: "center",
}}
/>
</g>
</g>
))}
{isOn ? (
<circle cx={hub.x} cy={hub.y} r={4} className={`mrs-hub ${r.pulse}`} />
) : null}
<circle
cx={hub.x}
cy={hub.y}
r={3.4}
className={isOn ? r.hubFill : "fill-slate-400 dark:fill-slate-600"}
/>
<circle
cx={hub.x}
cy={hub.y}
r={1.3}
className="fill-white dark:fill-slate-950"
/>
{isSel ? (
<g transform={`translate(${hub.x},${hub.y - 17})`}>
<rect
x={-labelW / 2}
y={-9}
width={labelW}
height={14}
rx={7}
className="fill-slate-900 dark:fill-slate-100"
/>
<text
x={0}
y={0.9}
textAnchor="middle"
className="fill-white dark:fill-slate-900"
style={{ fontSize: 8, fontWeight: 600, letterSpacing: 0.1 }}
>
{r.hub}
</text>
</g>
) : null}
</g>
);
})}
</svg>
<div className="mt-4 flex flex-wrap items-center gap-x-5 gap-y-2 border-t border-slate-200 pt-4 text-xs text-slate-500 dark:border-slate-800 dark:text-slate-400">
<span className="flex items-center gap-1.5">
<span
className="h-2 w-2 rounded-full bg-slate-400 dark:bg-slate-600"
aria-hidden="true"
/>
Hub offline
</span>
<span className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-sky-500" aria-hidden="true" />
Hub routing
</span>
<span className="ml-auto hidden sm:inline">
Click a landmass, or use the chips above with arrow keys.
</span>
</div>
</div>
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={active.id}
initial={reduce ? false : { opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 1 } : { opacity: 0, y: -10 }}
transition={{ duration: reduce ? 0 : 0.24, ease: [0.22, 1, 0.36, 1] }}
className="flex flex-col rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900/40"
>
<div className="flex items-start justify-between gap-3">
<div>
<div className="flex items-center gap-2">
<span
className={`h-2.5 w-2.5 rounded-full ${active.dot}`}
aria-hidden="true"
/>
<h3 className="text-lg font-semibold tracking-tight text-slate-900 dark:text-slate-50">
{active.name}
</h3>
</div>
<p className="mt-1.5 font-mono text-xs text-slate-500 dark:text-slate-400">
{active.code}
</p>
</div>
<span
className={[
"shrink-0 rounded-full border px-2.5 py-1 text-[11px] font-semibold",
active.status === "Live"
? "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"
: "border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300",
].join(" ")}
>
{active.status}
</span>
</div>
<dl className="mt-6 grid grid-cols-2 gap-px overflow-hidden rounded-2xl border border-slate-200 bg-slate-200 dark:border-slate-800 dark:bg-slate-800">
{[
{ k: "Primary hub", v: active.hub },
{ k: "Points of presence", v: String(active.pops) },
{ k: "p50 latency", v: `${active.latency} ms` },
{ k: "Egress", v: `$${active.rate.toFixed(3)}/GB` },
].map((s) => (
<div key={s.k} className="bg-white p-3.5 dark:bg-slate-900">
<dt className="text-[11px] font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400">
{s.k}
</dt>
<dd className="mt-1 text-sm font-semibold text-slate-900 dark:text-slate-100">
{s.v}
</dd>
</div>
))}
</dl>
<p className="mt-5 text-xs font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400">
Also serving
</p>
<ul className="mt-2.5 flex flex-wrap gap-1.5">
{active.cities.map((city) => (
<li
key={city}
className="rounded-md border border-slate-200 bg-slate-50 px-2 py-1 text-xs text-slate-600 dark:border-slate-800 dark:bg-slate-800/50 dark:text-slate-300"
>
{city}
</li>
))}
</ul>
<div className="mt-auto flex items-center justify-between gap-4 border-t border-slate-200 pt-5 dark:border-slate-800">
<div>
<span
id={`${uid}-sw`}
className="block text-sm font-medium text-slate-900 dark:text-slate-100"
>
Route traffic here
</span>
<span className="mt-0.5 block text-xs text-slate-500 dark:text-slate-400">
{active.capacity} peering capacity
</span>
</div>
<button
type="button"
role="switch"
aria-checked={activeOn}
aria-labelledby={`${uid}-sw`}
onClick={() => toggle(active.id)}
className={[
"relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
active.ring,
activeOn
? "border-transparent bg-slate-900 dark:bg-slate-100"
: "border-slate-300 bg-slate-200 dark:border-slate-700 dark:bg-slate-800",
].join(" ")}
>
<span
aria-hidden="true"
className={[
"inline-block h-4 w-4 transform rounded-full shadow-sm transition-transform duration-200",
activeOn
? "translate-x-6 bg-white dark:bg-slate-900"
: "translate-x-1 bg-white dark:bg-slate-500",
].join(" ")}
/>
</button>
</div>
</motion.div>
</AnimatePresence>
</div>
<div className="mt-6 flex flex-wrap items-center gap-x-8 gap-y-4 rounded-3xl border border-slate-200 bg-white px-6 py-5 shadow-sm dark:border-slate-800 dark:bg-slate-900/40">
{on.length === 0 ? (
<p className="text-sm text-slate-600 dark:text-slate-400">
No regions routing — every request falls back to your origin server.
</p>
) : (
<>
<Stat label="Regions" value={String(on.length)} />
<Stat label="Points of presence" value={String(totalPops)} />
<Stat label="Worst-case p50" value={`${worstLatency} ms`} />
<Stat label="Blended egress" value={`$${blended.toFixed(3)}/GB`} />
<ul className="ml-auto flex flex-wrap items-center gap-1.5">
{on.map((r) => (
<li key={r.id} className="flex items-center gap-1.5">
<span className={`h-1.5 w-1.5 rounded-full ${r.dot}`} aria-hidden="true" />
<span className="font-mono text-[11px] text-slate-500 dark:text-slate-400">
{r.code}
</span>
</li>
))}
</ul>
</>
)}
</div>
</div>
</section>
);
}
function Stat({ label, value }: { label: string; value: string }) {
return (
<div>
<p className="text-[11px] font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400">
{label}
</p>
<p className="mt-1 text-xl font-semibold tabular-nums tracking-tight text-slate-900 dark:text-slate-50">
{value}
</p>
</div>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Map Card
Originallocation card with a hand-drawn SVG map

Map Location Picker
Originallocation picker with pin

Map Store Locator
Originalstore locator list plus map

Map Contact
Originalcontact section with map and details
Map Delivery Tracking
Originaldelivery tracking with route

Map Pins List
Originalmap with a list of pinned places

Map Mini
Originalmini map preview widget

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero
OriginalA centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero
OriginalA dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.

