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";
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 arenullwhen empty.municipalityIdis the IBGE code — the value to persist.layout="column"stacks the selects (default is side by side).disabledlocks 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 whenonSelectis set).selectedaccepts a single UF or a list — handy for multi-select.- Each state has an
aria-labelwith 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 byidorname; accepts a list.values— choropleth by municipality (key =idorname).showLabels—falseby default: a state has hundreds of municipalities and labels overlap.showTooltip(defaulttrue) — 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
reverseGeocodedoes 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.nearestMunicipalitycompares 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.lengthequal bands.thresholdScale(thresholds, palette)— bands by cutoff (palettehasthresholds.length + 1colors).<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.
Part 7 — Zoom, color-by-region and municipality search
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 />
</>
);
}
ufrestricts 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 througharia-activedescendant),Enterpicks it,Esccloses the list andTableaves the field, closing the list. Results are not in theTaborder.
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'sutils/locations. - Selector:
BrazilStateCitySelectchains State → City. - National map:
BrazilMaprenders the 27 UFs in SVG — clickable (onSelect), highlightable (selected) and choropleth (values). No external tiles; bundled, lazy geometry. - State submap:
BrazilStateMapdraws 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/brsubpath.
See also
- Geolocation — lat/lon capture, trajectory and tile-free
TrajectoryMap - Forms BR — CPF/CNPJ/CEP and
useViaCEP - Components: Data entry —
Select,Combobox
