Web InnoventixFreeCode

Map Region Select

Original · free

clickable region selector map

byWeb InnoventixReact + Tailwind
mapregionselectmaps
Open

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
map-region-select.tsx
"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

motion

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 quote

Similar components

Browse all →