Skip to content

Brazil map & locations

A clickable national map of the 27 federative units + a states-and-cities dataset — with no paid or external API. The geometry is a simplified, bundled IBGE GeoJSON (rendered as SVG), and the locations list mirrors utils/locations from tempest-fastapi-sdk.

Import from the tempest-react-sdk/br subpath

This module bundles data (~5600 city names + UF geometry). To keep it off the bundle of apps that don't use it, it lives in a separate subpath — import from tempest-react-sdk/br, not the root. The map geometry still loads lazily (only when BrazilMap mounts).

import { BrazilMap, citiesByUf } from "tempest-react-sdk/br";

Mapa do Brasil (UF + cidades) in the gallery

Section brazil-map of the gallery — run it locally to interact.

When to use

  • A clickable Brazil map to pick a state (dashboards, regional filters).
  • Choropleth: tint states by a metric (sales, users, coverage).
  • A cascading State → City selector in forms.
  • Query states/cities/regions offline (citiesByUf, ufChoices, ...).

Part 1 — Locations data

Start with the data: pure, network-free functions available immediately.

import {
  listStates,
  getState,
  citiesByUf,
  statesByRegion,
  ufChoices,
  isValidUf,
  normalizeUf,
} from "tempest-react-sdk/br";

listStates().length; // 27 (sorted by name)

getState("sp");
// { uf: "SP", name: "São Paulo", region: "Sudeste", cities: [...] }

citiesByUf("RJ");     // ["Angra dos Reis", "Aperibé", ..., "Rio de Janeiro", ...]
citiesByUf("XX");     // [] — an invalid UF returns an empty list (never throws)

statesByRegion("Sul").map((s) => s.uf); // ["PR", "RS", "SC"]

normalizeUf(" rj "); // "RJ"
normalizeUf("zz");   // null
isValidUf("mg");     // true

Empty collections are not errors

citiesByUf of an unknown UF returns [], it does not throw. Same convention as the backend: "no matches" is a valid result.

The IBGE code is what you store

Municipality names change. Presidente Juscelino (RN) became Serra Caiada in 2013, Embu became Embu das Artes in 2011, Campo de Santana (PB) became Tacima in 2010. The 7-digit IBGE code does not — it is what identifies a municipality, and it is the key every dataset in this module joins on.

import { municipalitiesByUf, resolveMunicipality } from "tempest-react-sdk/br";

municipalitiesByUf("SP").slice(0, 2);
// [{ id: "3500105", name: "Adamantina" }, { id: "3500204", name: "Adolfo" }]

resolveMunicipality("SP", "São Paulo");
// { id: "3550308", name: "São Paulo" }

resolveMunicipality accepts four ways of writing the same place — which is the point, because an address saved five years ago has to keep pointing at it:

resolveMunicipality("RN", "Serra Caiada")?.id;         // "2410306" — current name
resolveMunicipality("RN", "Presidente Juscelino")?.id; // "2410306" — former name
resolveMunicipality("SP", "sao paulo")?.id;            // "3550308" — unaccented, lowercase
resolveMunicipality("DF", "Ceilândia")?.id;            // "5300108" — administrative region

Store the id, display the name

BrazilStateCitySelect emits both (municipalityId and city). Persisting the name works until the next rename; persisting the code always works — and it is what municipalityCentroid(id) and reverseGeocode hand back.

The Federal District has one municipality and 35 administrative regions

The DF has exactly one municipality: Brasília. Ceilândia, Taguatinga, Gama and the other 32 are administrative regions inside it — they have no municipality code and never had one.

Ignoring that is not an option: nobody in the DF writes "Brasília" in an address field. So they are listed and resolvable, in a field of their own, and they geocode through the municipality that contains them.

import { administrativeRegionsByUf, municipalitiesByUf } from "tempest-react-sdk/br";

municipalitiesByUf("DF");
// [{ id: "5300108", name: "Brasília" }]

administrativeRegionsByUf("DF").length; // 35
administrativeRegionsByUf("DF")[0];
// { id: "53001080521", name: "Arniqueira", municipalityId: "5300108" }

administrativeRegionsByUf("SP"); // [] — only the DF has any

IBGE names three of them as composites of what the local government signposts separately (SCIA is Estrutural; Sudoeste/Octogonal and Sol Nascente/Pôr do Sol are two each). The older names keep resolving:

resolveMunicipality("DF", "Octogonal")?.id;  // "5300108"
resolveMunicipality("DF", "Estrutural")?.id; // "5300108"

citiesByUf("DF") now returns only ["Brasília"]

Through v0.53.0 it returned 36 names, 35 of them administrative regions that did not geocode — picking "Ceilândia" produced a selection geocodeMunicipality answered empty. If your form needs to offer the regions, use administrativeRegionsByUf("DF") (BrazilStateCitySelect already does) and resolveMunicipality to validate.

Which IBGE vintage the data comes from

import { datasetVintage, pendingGeometryIds } from "tempest-react-sdk/br";

datasetVintage(); // { roster: "2026-08-29", mesh: "2022" }
pendingGeometryIds(); // ["5101837"]

The two vintages differ on purpose: IBGE publishes a new municipality before it publishes its boundary. pendingGeometryIds() enumerates exactly that gap — today one municipality, Boa Esperança do Norte (MT), installed in 2023, for which IBGE serves no geometry at any endpoint. It is listed and selectable; it is the one municipality geocodeMunicipality cannot place.

Why this is exposed

Because the alternative is your app discovering the gap as an empty result in production. A package test pins this list: when it grows without someone deciding, the build fails.

Feed a <Select> / <Combobox>

ufChoices() and cityChoices(uf) already return { value, label }:

import { Select } from "tempest-react-sdk";
import { ufChoices } from "tempest-react-sdk/br";

<Select label="State" placeholder="Select" options={ufChoices()} />;

Part 2 — State → City selector

BrazilStateCitySelect chains two selects: picking a state filters the cities of that UF. The city resets when the state changes.

import { BrazilStateCitySelect } from "tempest-react-sdk/br";

export function AddressForm() {
  return (
    <BrazilStateCitySelect
      onChange={({ uf, city, municipalityId }) => console.log(uf, city, municipalityId)}
      stateLabel="State"
      cityLabel="City"
    />
  );
}
  • defaultUf / defaultCity — initial values (uncontrolled).
  • onChange({ uf, city, municipalityId }) — fires on every change; all three are null when empty. municipalityId is the IBGE code — the value to persist.
  • layout="column" stacks the selects (default is side by side).
  • disabled locks both.

City enables only after a state

The city select is disabled until a state is chosen — there is nothing to list before that.

In the DF, the list includes the administrative regions

Picking "DF" lists Brasília and the 35 regions. Every one of them emits municipalityId: "5300108", so the selection always geocodes.


Part 3 — The BrazilMap

SVG map of the 27 UFs, with auto-fit, acronym labels and per-state clicking. No external tiles.

Clickable map

import { useState } from "react";
import { BrazilMap, type UF } from "tempest-react-sdk/br";

export function MapPicker() {
  const [uf, setUf] = useState<UF | null>(null);
  return (
    <>
      <BrazilMap selected={uf} onSelect={setUf} height={440} />
      {uf && <p>Selected: {uf}</p>}
    </>
  );
}
  • onSelect(uf) fires on click (and on Enter/Space — states are focusable when onSelect is set).
  • selected accepts a single UF or a list — handy for multi-select.
  • Each state has an aria-label with the name — accessible by default.

Hover tooltip

By default (showTooltip, default true) a floating tooltip appears on hover: name, acronym, region and city count — plus the choropleth value when values is set (e.g. São Paulo (SP) · Sudeste · 645 cidades). Pass showTooltip={false} to disable, or renderTooltip={(data) => ...} to customize the content (data = { uf, name, value? }).

<BrazilMap
  renderTooltip={({ uf, name, value }) => (
    <><strong>{name}</strong> — {value ?? "no data"}</>
  )}
/>

Choropleth (tint by metric)

Pass values (one number per UF) and each state is tinted linearly between minColor and maxColor:

import { BrazilMap } from "tempest-react-sdk/br";

const sales = { SP: 1200, MG: 640, RJ: 580, BA: 410, RS: 390 };

<BrazilMap values={sales} minColor="#e0f2fe" maxColor="#0369a1" showLabels={false} />;

States without a value use the base surface color.

Map + cities (full recipe)

The case that motivated the module: click the map and list the state's cities.

import { useState } from "react";
import {
  BrazilMap,
  BrazilStateCitySelect,
  getState,
  type UF,
} from "tempest-react-sdk/br";

export function NationalMap() {
  const [uf, setUf] = useState<UF | null>(null);
  const state = uf ? getState(uf) : null;

  return (
    <div>
      <BrazilMap selected={uf} onSelect={setUf} height={440} />

      {state && (
        <section>
          <h3>
            {state.name} — {state.cities.length} cities
          </h3>
          <BrazilStateCitySelect
            key={uf}
            defaultUf={uf!}
            onChange={({ city }) => console.log("city:", city)}
          />
        </section>
      )}
    </div>
  );
}

Reset on state change

The key={uf} remounts the selector when the UF changes from the map, ensuring the city resets.

BrazilMap props

(see also BrazilStateMap for the municipality level.)

Prop Type Default Description
selected UF \| UF[] \| null — Highlighted UF(s).
onSelect (uf: UF) => void — Click/keyboard on a state.
values Partial<Record<UF, number>> — Per-UF metric → choropleth.
minColor / maxColor string primary tints Choropleth scale ends.
height number 440 Viewport height in px.
padding number 12 Inner padding in px.
showLabels boolean true Acronym at each UF centroid.
showTooltip boolean true Floating hover tooltip (name + region + city count + value).
renderTooltip (data) => ReactNode — Custom tooltip content ({ uf, name, value? }).
label string "Mapa do Brasil por estado" Accessible region label.

Part 4 — State submap (BrazilStateMap)

A submap of one state with all its municipalities clickable. Municipal geometry is split per UF and loaded lazily — opening the SP map fetches only SP's chunk (~40-70 KB gzip), never the country's ~2 MB.

import { useState } from "react";
import { BrazilStateMap, type Municipality } from "tempest-react-sdk/br";

export function SPMunicipalities() {
  const [city, setCity] = useState<Municipality | null>(null);
  return (
    <>
      <BrazilStateMap uf="SP" selected={city?.name} onSelect={setCity} height={420} />
      {city && <p>{city.name} — IBGE {city.id}</p>}
    </>
  );
}
  • uf (required) — the state to draw.
  • onSelect({ id, name }) — fires on a municipality click (id = 7-digit IBGE code).
  • selected — matches by id or name; accepts a list.
  • values — choropleth by municipality (key = id or name).
  • showLabels — false by default: a state has hundreds of municipalities and labels overlap.
  • showTooltip (default true) — floating hover tooltip with name + IBGE code (+ choropleth value when present). renderTooltip={(data) => ...} customizes it (data = { id, name, value? }).

National → state drill-down (recipe)

Combine both maps: clicking the national map switches the submap's state.

import { useState } from "react";
import { BrazilMap, BrazilStateMap, type Municipality, type UF } from "tempest-react-sdk/br";

export function DrillDown() {
  const [uf, setUf] = useState<UF>("SP");
  const [city, setCity] = useState<Municipality | null>(null);

  return (
    <div style={{ display: "flex", gap: 16 }}>
      <BrazilMap
        selected={uf}
        onSelect={(u) => {
          setUf(u);
          setCity(null);
        }}
        height={320}
      />
      <BrazilStateMap uf={uf} selected={city?.name} onSelect={setCity} height={320} />
    </div>
  );
}

Municipal choropleth

import { BrazilStateMap } from "tempest-react-sdk/br";

<BrazilStateMap
  uf="RJ"
  values={{ "Rio de Janeiro": 100, Niterói: 40, "Duque de Caxias": 55 }}
/>;

Map names vs. names dataset

Municipality names on the map come from the IBGE GeoJSON; citiesByUf comes from the names dataset. They are nearly identical, but spelling/accents may differ in rare cases. To match values, prefer the IBGE code (id) when you have it.

Direct municipal-geometry access

import { loadStateMunicipalities } from "tempest-react-sdk/br";

const sp = await loadStateMunicipalities("SP");
sp?.features.length; // 644

Part 5 — Offline geocoding

Convert between name/coordinate and municipality, offline. Uses a compact centroid index (~97 KB gzip) loaded lazily — no network calls, no API key.

import {
  reverseGeocode,
  nearestMunicipality,
  geocodeMunicipality,
  municipalityCentroid,
  stateCentroid,
} from "tempest-react-sdk/br";

// Coordinate → the municipality that CONTAINS it (point-in-polygon, exact):
await reverseGeocode({ latitude: -23.5505, longitude: -46.6333 });
// { id: "3550308", name: "São Paulo", uf: "SP" }

// Coordinate → nearest-centroid municipality (fast, approximate, no geometry):
await nearestMunicipality({ latitude: -23.55, longitude: -46.63 });
// { id, name, uf: "SP", latitude, longitude, distanceKm }

// Name → coordinate (homonyms may exist across states):
await geocodeMunicipality("Bonito");          // several
await geocodeMunicipality("São Paulo", "SP"); // filtered by UF

// Centroids:
await municipalityCentroid("3550308"); // { id, name, uf, latitude, longitude }
await stateCentroid("SP");             // { latitude, longitude }

reverseGeocode vs nearestMunicipality

  • reverseGeocode does point-in-polygon → returns the municipality that actually contains the point. Loads one state's geometry (lazy per-UF chunk). Pass { uf } if you know it, to skip candidate-state detection.
  • nearestMunicipality compares centroids only → fast and geometry-free, but near borders / in large municipalities it can pick a neighbor.

Recipe: "where am I?" (GPS → municipality)

Combine with usePositionTracker from the Geolocation module:

import { useEffect, useState } from "react";
import { usePositionTracker } from "tempest-react-sdk";
import { reverseGeocode, type ReverseGeocodeResult } from "tempest-react-sdk/br";

export function WhereAmI() {
  const { lastPoint } = usePositionTracker({ autoStart: true });
  const [place, setPlace] = useState<ReverseGeocodeResult | null>(null);

  useEffect(() => {
    if (lastPoint) reverseGeocode(lastPoint).then(setPlace);
  }, [lastPoint]);

  return <p>{place ? `You are in ${place.name} — ${place.uf}` : "Locating…"}</p>;
}

Accuracy

The geometry is simplified (~2 km). Points within ~1-2 km of a border may resolve to the neighboring municipality; points offshore / outside the territory fall back to the nearest centroid.


Part 6 — Markers, color scales and legend

Markers (pins)

BrazilMap, BrazilStateMap and TrajectoryMap (the Geolocation module) accept markers — { latitude, longitude } points plotted over the map, with label (tooltip), color, radius and id. onMarkerClick(marker, index) on click.

import { BrazilMap, type GeoMarker } from "tempest-react-sdk/br";

const capitals: GeoMarker[] = [
  { id: "sp", latitude: -23.55, longitude: -46.63, label: "São Paulo", color: "#e11d48" },
  { id: "rj", latitude: -22.91, longitude: -43.17, label: "Rio de Janeiro" },
];

<BrazilMap markers={capitals} onMarkerClick={(m) => console.log(m.label)} />;

Pins from geocoding

Combine with geocoding (Part 5): municipalityCentroid(id) or geocodeMunicipality(name) give the coordinates to turn into markers.

Color scales + legend

For a choropleth beyond the 2-color ramp, pass colorScale (from sequentialScale/quantizeScale/thresholdScale) and pair it with <MapLegend>. Built-in colorblind-safe palettes: SEQUENTIAL_BLUES, SEQUENTIAL_GREENS, SEQUENTIAL_VIRIDIS, DIVERGING_RDBU.

import {
  BrazilMap,
  MapLegend,
  sequentialScale,
  SEQUENTIAL_VIRIDIS,
} from "tempest-react-sdk/br";

const sales = { SP: 1200, MG: 640, RJ: 580, BA: 410, RS: 390 };
const scale = sequentialScale(0, 1200, SEQUENTIAL_VIRIDIS);

<div>
  <BrazilMap values={sales} colorScale={scale} showLabels={false} />
  <MapLegend title="Sales (R$ k)" min={0} max={1200} palette={SEQUENTIAL_VIRIDIS} />
</div>;
  • sequentialScale(min, max, palette) — continuous gradient.
  • quantizeScale(min, max, palette) — palette.length equal bands.
  • thresholdScale(thresholds, palette) — bands by cutoff (palette has thresholds.length + 1 colors).
  • <MapLegend> — continuous gradient (min/max/palette + format) or discrete bands (items={[{ color, label }]}).

Brand palette

The palettes are public standards (ColorBrewer/Viridis). Swap for any ordered list of your brand's hex colors — the scale builders accept any string[].

An empty palette throws at construction

quantizeScale and thresholdScale throw when the palette has no colours, rather than returning a scale that answers undefined for every value. Such a scale reaches the DOM as fill="undefined": the map comes out blank and nothing is reported. The error now lands on the line that built the scale, which is where the mistake is — usually a palette coming out of a .filter() or a .slice() with the wrong index.


Pan & zoom

zoomable enables wheel-zoom (anchored at the cursor) + drag-pan on BrazilMap/BrazilStateMap. Double-click resets; a Reset button appears while zoomed/panned.

<BrazilStateMap uf="MG" zoomable />

Color by region

colorByRegion tints each state by its macro-region (categorical), overriding values/colorScale. Pair it with a discrete legend via regionLegendItems().

import { BrazilMap, MapLegend, regionLegendItems } from "tempest-react-sdk/br";

<div>
  <BrazilMap colorByRegion showLabels={false} />
  <MapLegend title="Region" items={regionLegendItems()} />
</div>;

REGION_COLORS exposes the region → color map (swap for your brand palette).

Municipality search (autocomplete)

MunicipalitySearch is an offline autocomplete (uses searchMunicipalities, debounced). Wire onSelect to a BrazilStateMap's selected to highlight it on the map.

import { useState } from "react";
import { MunicipalitySearch, BrazilStateMap, type UF } from "tempest-react-sdk/br";

export function SearchOnMap() {
  const [uf] = useState<UF>("SP");
  const [city, setCity] = useState<string | null>(null);
  return (
    <>
      <MunicipalitySearch uf={uf} label="Municipality" onSelect={(m) => setCity(m.name)} />
      <BrazilStateMap uf={uf} selected={city} zoomable />
    </>
  );
}
  • uf restricts the search to one state; without it, it searches the whole country (the result shows the UF).
  • onSelect(m) receives { id, name, uf, latitude, longitude } — enough to also center/mark it.
  • Keyboard matches Combobox: focus stays on the field, ↓/↑ move the active result (announced through aria-activedescendant), Enter picks it, Esc closes the list and Tab leaves the field, closing the list. Results are not in the Tab order.

About the geometry

  • Source: IBGE mesh (/api/v3/malhas, 2022 vintage), simplified with Douglas-Peucker (~2 km tolerance, capped at a twentieth of each ring's diagonal so a small municipality is not erased) and rounded to 3 decimals.
  • Size: ~99 KB raw / ~31 KB gzip, in a separate chunk loaded lazily by BrazilMap.
  • Accuracy: adequate for a clickable overview map, not for precise geographic analysis or area computation.

Municipality: use BrazilStateMap

BrazilMap draws states. For the municipality level, BrazilStateMap draws every municipality of a state — the municipal geometry (~2 MB total) is split per UF and loaded lazily, one chunk per state, so it never lands in a single bundle.

Advanced geometry access

Need the GeoJSON for a custom render? Load it lazily:

import { loadBrUfGeoJson } from "tempest-react-sdk/br";

const collection = await loadBrUfGeoJson();
collection.features.length; // 27

Recap

  • Data: listStates, getState, citiesByUf, statesByRegion, ufChoices, cityChoices, isValidUf, normalizeUf, isValidCity — offline, mirroring the FastAPI SDK's utils/locations.
  • Selector: BrazilStateCitySelect chains State → City.
  • National map: BrazilMap renders the 27 UFs in SVG — clickable (onSelect), highlightable (selected) and choropleth (values). No external tiles; bundled, lazy geometry.
  • State submap: BrazilStateMap draws every municipality of a UF — clickable, choropleth, per-state geometry loaded lazily. loadStateMunicipalities(uf) exposes the raw geometry.
  • Import always from the tempest-react-sdk/br subpath.

See also