Choropleth
Regions coloured by a data value, with outlines and hover highlighting.
Usage
<Choropleth> builds on <Layer>: give it polygons, the property to colour by, and a ramp. It renders the fill, the borders, and the hover highlight for you.
"use client";
import type { GeoJSONFeature } from "mapbox-gl";
import { useState } from "react";
import { Choropleth, type ChoroplethRamp } from "@/components/ui/choropleth";
import { Legend } from "@/components/ui/legend";
import { Map } from "@/components/ui/map";
// One ramp object, shared by the map and the legend - they cannot drift.
const ramp = {
kind: "step",
breaks: [7, 12, 17],
colors: ["#eef2f6", "#ccd6e0", "#9fb1c2", "#6d859c"],
} satisfies ChoroplethRamp;
export function ChoroplethExample() {
const [feature, setFeature] = useState<GeoJSONFeature | null>(null);
const area = feature ? Number(feature.properties?.area) : null;
return (
<div className="relative h-[460px] w-full">
<Map center={[-104.996, 39.737]} zoom={9.9}>
<Choropleth
id="zips"
data="/data/denver-zips.geojson"
value="area"
ramp={ramp}
featureId="zip"
opacity={0.55}
onHover={setFeature}
/>
</Map>
<Legend
ramp={ramp}
title="ZIP area · km²"
activeValue={area}
className="absolute top-3 left-3 z-10"
/>
<div className="bg-background/85 absolute bottom-3 left-3 z-10 rounded-md border px-2.5 py-1 text-xs backdrop-blur">
{feature ? (
<span>
<span className="font-mono font-semibold">
{String(feature.properties?.zip)}
</span>
<span className="text-muted-foreground">
{" "}
· {area?.toFixed(1)} km²
</span>
</span>
) : (
<span className="text-muted-foreground">Hover a ZIP</span>
)}
</div>
<div className="bg-background/85 text-muted-foreground absolute right-3 bottom-3 z-10 rounded-md border px-2 py-1 text-[11px] backdrop-blur">
40 central Denver ZIPs
</div>
</div>
);
}
Ramps
A ramp is a plain object, not an expression. That matters: the same object drives the paint expression and <Legend>, so the swatches can never drift from the colours actually on the map.
const ramp: ChoroplethRamp = {
kind: "step",
breaks: [7, 12, 17],
colors: ["#eef2f6", "#ccd6e0", "#9fb1c2", "#6d859c"],
};Three kinds are supported:
{ kind: "step", breaks: [10, 20], colors: ["#eee", "#88f", "#00a"] }
{ kind: "linear", stops: [[0, "#eee"], [100, "#00a"]] }
{ kind: "categorical", values: [["a", "#f00"], ["b", "#0f0"]], fallback: "#ccc" }step for skewed data. Real world values are rarely evenly spread - a linear ramp over areas or populations collapses most features into one shade. Quantile breaks with step keep the map readable.Hover
Hover highlighting uses Mapbox feature-state, which needs a stable feature id. GeoJSON features rarely carry one, so name the property that holds it with featureId:
<Choropleth id="zips" data={zips} value="area" ramp={ramp} featureId="zip" />This is set as promoteId on the source. The highlight is then pure GPU state - no re-render, no setData, nothing recomputed per frame. Omit featureId and highlighting turns off, though onHover and onSelect still fire.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Names the source and the layers derived from it. |
data | GeoJSON | string | — | GeoJSON object or a URL. |
value | string | Expression | — | Property name to colour by, or a full Mapbox expression. |
ramp | ChoroplethRamp | — | Colour scale: step, linear or categorical. Share it with Legend. |
featureId | string | — | Property holding a stable id. Required for hover highlighting. |
opacity | number | 0.75 | Fill opacity. Hovered features get a slight bump. |
outline | boolean | { color?, width? } | true | Borders between features. |
hover | boolean | true | Highlight the feature under the cursor. |
onHover / onSelect | (feature, event) => void | — | Fired on mousemove and click. onHover receives null on leave. |