tessl install tessl/npm-react-leaflet@5.0.3React components for Leaflet maps
Components for drawing geometric shapes on the map including circles, lines, polygons, and rectangles. All shape components support styling through path options and can contain popups and tooltips.
Circular shape with radius specified in meters.
/**
* Component for displaying circles on the map (radius in meters)
* @param props - Circle properties
*/
const Circle: FunctionComponent<CircleProps>;
interface CircleProps extends CircleOptions, PathProps {
/** Center point of the circle (required) */
center: LatLngExpression;
/** Radius in meters */
radius?: number;
/** Child components (Popup, Tooltip) */
children?: ReactNode;
}Usage Example:
import { MapContainer, TileLayer, Circle } from "react-leaflet";
function Map() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Circle
center={[51.505, -0.09]}
radius={500}
pathOptions={{ color: "red", fillColor: "blue", fillOpacity: 0.5 }}
/>
</MapContainer>
);
}Circular marker with radius specified in pixels (constant size regardless of zoom).
/**
* Component for displaying circle markers (radius in pixels)
* @param props - Circle marker properties
*/
const CircleMarker: FunctionComponent<CircleMarkerProps>;
interface CircleMarkerProps extends CircleMarkerOptions, PathProps {
/** Center point of the circle marker (required) */
center: LatLngExpression;
/** Radius in pixels */
radius?: number;
/** Child components (Popup, Tooltip) */
children?: ReactNode;
}Usage Example:
<CircleMarker
center={[51.505, -0.09]}
radius={20}
pathOptions={{ color: "green", fillColor: "yellow" }}
>
<Popup>A circle marker</Popup>
</CircleMarker>Multi-segment line connecting multiple points.
/**
* Component for displaying polylines (multi-segment lines)
* @param props - Polyline properties
*/
const Polyline: FunctionComponent<PolylineProps>;
interface PolylineProps extends PolylineOptions, PathProps {
/** Array of positions defining the line segments (required) */
positions: LatLngExpression[] | LatLngExpression[][];
/** Child components (Popup, Tooltip) */
children?: ReactNode;
}Usage Example:
const positions = [
[51.505, -0.09],
[51.51, -0.1],
[51.51, -0.12],
[51.52, -0.11],
];
<Polyline
positions={positions}
pathOptions={{ color: "blue", weight: 3 }}
>
<Popup>A blue polyline</Popup>
</Polyline>Multi-Line Polyline:
const multiLine = [
[[51.5, -0.1], [51.51, -0.11], [51.52, -0.12]],
[[51.5, -0.05], [51.51, -0.06], [51.52, -0.07]],
];
<Polyline positions={multiLine} pathOptions={{ color: "red" }} />Filled polygon shape with optional holes.
/**
* Component for displaying polygons (closed shapes with fill)
* @param props - Polygon properties
*/
const Polygon: FunctionComponent<PolygonProps>;
interface PolygonProps extends PolylineOptions, PathProps {
/** Array of positions defining the polygon vertices (required) */
positions: LatLngExpression[] | LatLngExpression[][] | LatLngExpression[][][];
/** Child components (Popup, Tooltip) */
children?: ReactNode;
}Simple Polygon:
const polygonPositions = [
[51.515, -0.09],
[51.52, -0.1],
[51.52, -0.12],
];
<Polygon
positions={polygonPositions}
pathOptions={{ color: "purple", fillColor: "pink" }}
>
<Popup>A triangular polygon</Popup>
</Polygon>Polygon with Holes:
const outerRing = [
[51.515, -0.09],
[51.52, -0.1],
[51.52, -0.12],
[51.515, -0.11],
];
const hole = [
[51.517, -0.1],
[51.518, -0.105],
[51.517, -0.11],
];
<Polygon positions={[outerRing, hole]} />Rectangular shape defined by geographic bounds.
/**
* Component for displaying rectangles
* @param props - Rectangle properties
*/
const Rectangle: FunctionComponent<RectangleProps>;
interface RectangleProps extends PathOptions, PathProps {
/** Geographic bounds of the rectangle (required) */
bounds: LatLngBoundsExpression;
/** Child components (Popup, Tooltip) */
children?: ReactNode;
}Usage Example:
const bounds = [
[51.49, -0.08],
[51.5, -0.06],
];
<Rectangle
bounds={bounds}
pathOptions={{ color: "orange", weight: 2 }}
>
<Popup>A rectangular area</Popup>
</Rectangle>interface PathProps extends InteractiveLayerProps {
/** Styling options for the path */
pathOptions?: PathOptions;
}
interface InteractiveLayerProps extends LayerProps, InteractiveLayerOptions {
interactive?: boolean;
bubblingMouseEvents?: boolean;
}// From Leaflet
interface PathOptions {
/** Whether to draw stroke along the path */
stroke?: boolean;
/** Stroke color */
color?: string;
/** Stroke width in pixels */
weight?: number;
/** Stroke opacity (0.0 - 1.0) */
opacity?: number;
/** Line cap shape */
lineCap?: LineCapShape;
/** Line join shape */
lineJoin?: LineJoinShape;
/** Dash pattern for stroke (e.g., "10, 5" for 10px dash, 5px gap) */
dashArray?: string;
/** Distance into the dash pattern to start dash */
dashOffset?: string;
/** Whether to fill the path with color */
fill?: boolean;
/** Fill color (defaults to color if not specified) */
fillColor?: string;
/** Fill opacity (0.0 - 1.0) */
fillOpacity?: number;
/** Fill rule for complex polygons */
fillRule?: FillRule;
/** Custom class name for the path element */
className?: string;
}
type LineCapShape = 'butt' | 'round' | 'square';
type LineJoinShape = 'miter' | 'round' | 'bevel';
type FillRule = 'nonzero' | 'evenodd';// From Leaflet (extends PathOptions)
interface CircleOptions extends PathOptions {
/** Radius of the circle in meters */
radius?: number;
}// From Leaflet (extends PathOptions)
interface CircleMarkerOptions extends PathOptions {
/** Radius of the circle marker in pixels */
radius?: number;
}// From Leaflet (extends PathOptions)
interface PolylineOptions extends PathOptions {
/** Simplification tolerance in pixels */
smoothFactor?: number;
/** Whether to disable polyline clipping */
noClip?: boolean;
}<Circle
center={[51.505, -0.09]}
radius={500}
pathOptions={{
color: "red",
fillColor: "blue",
fillOpacity: 0.5,
weight: 2,
}}
/><Polyline
positions={positions}
pathOptions={{
color: "blue",
weight: 3,
dashArray: "10, 5",
}}
/><Polygon
positions={positions}
pathOptions={{
color: "green",
fill: false,
weight: 3,
}}
/><Polyline
positions={positions}
pathOptions={{
color: "purple",
weight: 5,
lineCap: "round",
lineJoin: "round",
}}
/>Circle vs CircleMarker:
Position Updates: All shapes update when their position/bounds props change:
<Circle center={currentCenter} radius={currentRadius} />Multi-Part Shapes: Polylines and Polygons can have multiple parts:
// Multi-line polyline
<Polyline positions={[line1, line2, line3]} />
// Polygon with holes
<Polygon positions={[outerRing, hole1, hole2]} />Event Handling: All shapes support event handlers:
<Polygon
positions={positions}
eventHandlers={{
click: (e) => console.log("Polygon clicked", e),
mouseover: (e) => console.log("Mouse over"),
mouseout: (e) => console.log("Mouse out"),
}}
/>Interactive Shapes: Control interactivity with the interactive prop:
<Circle center={pos} radius={500} interactive={false} />Children Support: All shapes can contain Popups and Tooltips:
<Polyline positions={positions}>
<Popup>Line information</Popup>
<Tooltip>Hover text</Tooltip>
</Polyline>Path Options vs Direct Props: Use pathOptions for styling:
// Correct
<Circle center={pos} radius={500} pathOptions={{ color: "red" }} />
// Also works (inherited from PathOptions)
<Circle center={pos} radius={500} color="red" />Bounds Expression: Rectangles accept flexible bound formats:
// Array format
<Rectangle bounds={[[51.49, -0.08], [51.5, -0.06]]} />
// LatLngBounds object
<Rectangle bounds={new LatLngBounds([51.49, -0.08], [51.5, -0.06])} />const hotspots = [
{ position: [51.505, -0.09], intensity: 0.8 },
{ position: [51.51, -0.1], intensity: 0.6 },
{ position: [51.52, -0.08], intensity: 0.4 },
];
function HeatMap() {
return (
<>
{hotspots.map((spot, idx) => (
<Circle
key={idx}
center={spot.position}
radius={300}
pathOptions={{
fillColor: "red",
fillOpacity: spot.intensity,
color: "red",
weight: 1,
}}
/>
))}
</>
);
}function InteractivePath() {
const [color, setColor] = useState("blue");
return (
<Polyline
positions={positions}
pathOptions={{ color, weight: 5 }}
eventHandlers={{
click: () => setColor(color === "blue" ? "red" : "blue"),
}}
>
<Popup>Click the line to change color</Popup>
</Polyline>
);
}function RouteDisplay({ route }) {
return (
<>
<Polyline
positions={route.path}
pathOptions={{ color: "blue", weight: 4 }}
>
<Popup>
<div>
<h4>{route.name}</h4>
<p>Distance: {route.distance} km</p>
</div>
</Popup>
</Polyline>
<CircleMarker
center={route.path[0]}
radius={8}
pathOptions={{ color: "green", fillColor: "green" }}
>
<Tooltip permanent>Start</Tooltip>
</CircleMarker>
<CircleMarker
center={route.path[route.path.length - 1]}
radius={8}
pathOptions={{ color: "red", fillColor: "red" }}
>
<Tooltip permanent>End</Tooltip>
</CircleMarker>
</>
);
}function ZonedArea() {
const zones = [
{ bounds: [[51.49, -0.12], [51.50, -0.10]], color: "green", name: "Safe Zone" },
{ bounds: [[51.50, -0.12], [51.51, -0.10]], color: "yellow", name: "Caution Zone" },
{ bounds: [[51.51, -0.12], [51.52, -0.10]], color: "red", name: "Danger Zone" },
];
return (
<>
{zones.map((zone, idx) => (
<Rectangle
key={idx}
bounds={zone.bounds}
pathOptions={{
color: zone.color,
fillColor: zone.color,
fillOpacity: 0.3,
}}
>
<Popup>{zone.name}</Popup>
</Rectangle>
))}
</>
);
}When rendering many vector shapes (>1000), use Canvas renderer instead of SVG:
<MapContainer preferCanvas={true} center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{/* Thousands of shapes render faster with Canvas */}
{shapes.map(shape => (
<Circle key={shape.id} center={shape.center} radius={shape.radius} />
))}
</MapContainer>const MemoizedCircle = React.memo(({ center, radius, color }) => (
<Circle
center={center}
radius={radius}
pathOptions={{ color, fillColor: color, fillOpacity: 0.5 }}
/>
));
function OptimizedShapes({ shapes }) {
return (
<>
{shapes.map(shape => (
<MemoizedCircle
key={shape.id}
center={shape.center}
radius={shape.radius}
color={shape.color}
/>
))}
</>
);
}<Polygon
positions={complexPositions}
pathOptions={{
smoothFactor: 2.0, // Higher value = more simplification
}}
/>function ZoomDependentShapes() {
const [zoom, setZoom] = useState(13);
return (
<MapContainer center={[51.505, -0.09]} zoom={zoom}>
<MapEvents onZoomChange={setZoom} />
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{zoom > 12 && (
// Only render detailed shapes at high zoom levels
<Circle center={[51.505, -0.09]} radius={100} />
)}
</MapContainer>
);
}function AnimatedCircle({ center, radius }) {
const [currentRadius, setCurrentRadius] = useState(0);
useEffect(() => {
let frame = 0;
const animate = () => {
frame++;
setCurrentRadius(Math.sin(frame * 0.1) * radius + radius);
if (frame < 100) {
requestAnimationFrame(animate);
}
};
animate();
}, [radius]);
return (
<Circle
center={center}
radius={currentRadius}
pathOptions={{ color: "blue", fillOpacity: 0.3 }}
/>
);
}function EditablePolygon() {
const [positions, setPositions] = useState([
[51.515, -0.09],
[51.52, -0.1],
[51.52, -0.12],
]);
const handleClick = (e) => {
setPositions([...positions, [e.latlng.lat, e.latlng.lng]]);
};
return (
<>
<Polygon
positions={positions}
pathOptions={{ color: "purple" }}
eventHandlers={{
click: handleClick,
}}
/>
{positions.map((pos, idx) => (
<CircleMarker
key={idx}
center={pos}
radius={5}
draggable={true}
eventHandlers={{
dragend: (e) => {
const newPos = e.target.getLatLng();
const newPositions = [...positions];
newPositions[idx] = [newPos.lat, newPos.lng];
setPositions(newPositions);
},
}}
/>
))}
</>
);
}function GradientCircle({ center, radius }) {
return (
<>
<defs>
<radialGradient id="grad1">
<stop offset="0%" style={{ stopColor: "rgb(255,255,0)", stopOpacity: 1 }} />
<stop offset="100%" style={{ stopColor: "rgb(255,0,0)", stopOpacity: 1 }} />
</radialGradient>
</defs>
<Circle
center={center}
radius={radius}
pathOptions={{
fillColor: "url(#grad1)",
fillOpacity: 0.7,
}}
/>
</>
);
}Solutions:
Solutions:
preferCanvas={true} in MapContainersmoothFactorSolutions:
Solutions:
interactive={true} (default for most shapes)import { render } from '@testing-library/react';
import { MapContainer, Circle } from 'react-leaflet';
test('renders circle shape', () => {
const { container } = render(
<MapContainer center={[51.505, -0.09]} zoom={13}>
<Circle
center={[51.505, -0.09]}
radius={500}
pathOptions={{ color: 'red' }}
/>
</MapContainer>
);
expect(container).toBeInTheDocument();
});test('handles shape click events', () => {
const handleClick = jest.fn();
render(
<MapContainer center={[51.505, -0.09]} zoom={13}>
<Polygon
positions={[[51.515, -0.09], [51.52, -0.1], [51.52, -0.12]]}
eventHandlers={{ click: handleClick }}
/>
</MapContainer>
);
// Simulate interaction
});<Circle
center={[51.505, -0.09]}
radius={500}
// Add title for accessibility
>
<Tooltip permanent>Area of interest</Tooltip>
</Circle><Polygon
positions={positions}
interactive={true}
pathOptions={{ color: 'blue' }}
>
<Popup>
<div role="dialog" aria-label="Selected area details">
<h3>Area Information</h3>
<p>Additional details</p>
</div>
</Popup>
</Polygon>import type { LatLngExpression, PathOptions } from 'leaflet';
interface CircleData {
center: LatLngExpression;
radius: number;
style: PathOptions;
}
const circleData: CircleData = {
center: [51.505, -0.09],
radius: 500,
style: {
color: 'red',
fillColor: 'blue',
fillOpacity: 0.5,
},
};
<Circle
center={circleData.center}
radius={circleData.radius}
pathOptions={circleData.style}
/>