tessl install tessl/npm-react-leaflet@5.0.3React components for Leaflet maps
UI control components for map interaction including zoom controls, scale display, attribution, and layer switching.
Control for switching between base layers and toggling overlay layers.
/**
* Component for layer switching control (compound component with sub-components)
* @param props - Layers control properties
*/
const LayersControl: ForwardRefExoticComponent<
LayersControlProps & RefAttributes<Control.Layers>
> & {
BaseLayer: FunctionComponent<ControlledLayerProps>;
Overlay: FunctionComponent<ControlledLayerProps>;
};
interface LayersControlProps extends Control.LayersOptions {
/** Child BaseLayer and Overlay components */
children?: ReactNode;
}Usage Example:
import { MapContainer, TileLayer, LayersControl, Marker } from "react-leaflet";
function Map() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<LayersControl position="topright">
<LayersControl.BaseLayer checked name="OpenStreetMap">
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Satellite">
<TileLayer
attribution='Tiles © Esri'
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
/>
</LayersControl.BaseLayer>
<LayersControl.Overlay name="Markers">
<Marker position={[51.505, -0.09]} />
</LayersControl.Overlay>
</LayersControl>
</MapContainer>
);
}Sub-component for defining base layers (only one can be active at a time).
/**
* Base layer option in LayersControl (radio button behavior)
* @param props - Controlled layer properties
*/
const BaseLayer: FunctionComponent<ControlledLayerProps>;
interface ControlledLayerProps {
/** Whether this layer is initially checked */
checked?: boolean;
/** Child layer component (TileLayer, etc.) */
children: ReactNode;
/** Display name in the control */
name: string;
}Sub-component for defining overlay layers (multiple can be active).
/**
* Overlay layer option in LayersControl (checkbox behavior)
* @param props - Controlled layer properties
*/
const Overlay: FunctionComponent<ControlledLayerProps>;Usage Example:
<LayersControl>
<LayersControl.Overlay checked name="Cities">
<LayerGroup>
<Marker position={[51.505, -0.09]} />
<Marker position={[51.51, -0.1]} />
</LayerGroup>
</LayersControl.Overlay>
<LayersControl.Overlay name="Roads">
<Polyline positions={roadPath} />
</LayersControl.Overlay>
</LayersControl>Zoom in and zoom out buttons.
/**
* Component for zoom control buttons
* @param props - Zoom control properties
*/
const ZoomControl: FunctionComponent<ZoomControlProps>;
type ZoomControlProps = Control.ZoomOptions;Usage Example:
import { MapContainer, TileLayer, ZoomControl } from "react-leaflet";
function Map() {
return (
<MapContainer
center={[51.505, -0.09]}
zoom={13}
zoomControl={false} // Disable default zoom control
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<ZoomControl position="bottomright" />
</MapContainer>
);
}Displays the map scale in metric and/or imperial units.
/**
* Component for displaying map scale
* @param props - Scale control properties
*/
const ScaleControl: FunctionComponent<ScaleControlProps>;
type ScaleControlProps = Control.ScaleOptions;Usage Example:
import { MapContainer, TileLayer, ScaleControl } 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" />
<ScaleControl position="bottomleft" metric={true} imperial={false} />
</MapContainer>
);
}Displays attribution text for map layers and data sources.
/**
* Component for displaying attribution
* @param props - Attribution control properties
*/
const AttributionControl: FunctionComponent<AttributionControlProps>;
type AttributionControlProps = Control.AttributionOptions;Usage Example:
import { MapContainer, TileLayer, AttributionControl } from "react-leaflet";
function Map() {
return (
<MapContainer
center={[51.505, -0.09]}
zoom={13}
attributionControl={false} // Disable default attribution
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<AttributionControl position="bottomright" prefix="My Map" />
</MapContainer>
);
}// From Leaflet
namespace Control {
interface LayersOptions extends ControlOptions {
/** Whether control starts collapsed */
collapsed?: boolean;
/** Whether to auto expand control on hover */
autoZIndex?: boolean;
/** Whether to hide single base layer */
hideSingleBase?: boolean;
/** Whether to sort layers alphabetically */
sortLayers?: boolean;
/** Function to sort layers */
sortFunction?: (
layerA: Layer,
layerB: Layer,
nameA: string,
nameB: string
) => number;
}
}// From Leaflet
namespace Control {
interface ZoomOptions extends ControlOptions {
/** Text for zoom in button */
zoomInText?: string;
/** Title for zoom in button */
zoomInTitle?: string;
/** Text for zoom out button */
zoomOutText?: string;
/** Title for zoom out button */
zoomOutTitle?: string;
}
}// From Leaflet
namespace Control {
interface ScaleOptions extends ControlOptions {
/** Maximum width of the control in pixels */
maxWidth?: number;
/** Whether to show metric scale */
metric?: boolean;
/** Whether to show imperial scale */
imperial?: boolean;
/** Whether to update scale on move end (false = update continuously) */
updateWhenIdle?: boolean;
}
}// From Leaflet
namespace Control {
interface AttributionOptions extends ControlOptions {
/** Prefix text before attributions */
prefix?: string | boolean;
}
}// From Leaflet
interface ControlOptions {
/** Position of the control */
position?: ControlPosition;
}
type ControlPosition =
| "topleft"
| "topright"
| "bottomleft"
| "bottomright";
// Default z-index values for control positions:
// topleft/topright: z-index 1000
// bottomleft/bottomright: z-index 1000Control Positions: All controls accept a position prop with four options:
"topleft" (default for most)"topright""bottomleft" (default for scale)"bottomright"Default Controls: MapContainer includes zoom and attribution controls by default. Disable them to use custom ones:
<MapContainer zoomControl={false} attributionControl={false}>
<ZoomControl position="bottomright" />
<AttributionControl position="topleft" />
</MapContainer>LayersControl Structure: LayersControl requires specific structure:
LayersControl.BaseLayer and/or LayersControl.Overlay componentsname propBase Layer Behavior: Only one base layer can be active at a time (radio button behavior):
<LayersControl.BaseLayer checked name="Default">
{/* This layer starts active */}
</LayersControl.BaseLayer>Overlay Behavior: Multiple overlays can be active simultaneously (checkbox behavior):
<LayersControl.Overlay checked name="Markers">
{/* Starts visible */}
</LayersControl.Overlay>
<LayersControl.Overlay name="Routes">
{/* Starts hidden */}
</LayersControl.Overlay>Collapsed Control: LayersControl can start collapsed:
<LayersControl collapsed={true} position="topright">Scale Units: ScaleControl can show metric, imperial, or both:
<ScaleControl metric={true} imperial={true} />Attribution Prefix: Customize or remove attribution prefix:
<AttributionControl prefix="Powered by Leaflet" />
<AttributionControl prefix={false} /> {/* No prefix */}Custom Control Text: Customize zoom button text:
<ZoomControl
zoomInText="+"
zoomOutText="-"
zoomInTitle="Zoom in"
zoomOutTitle="Zoom out"
/>function MapWithControls() {
return (
<MapContainer
center={[51.505, -0.09]}
zoom={13}
zoomControl={false}
attributionControl={false}
>
<LayersControl position="topright">
<LayersControl.BaseLayer checked name="Street Map">
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</LayersControl.BaseLayer>
<LayersControl.BaseLayer name="Satellite">
<TileLayer url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}" />
</LayersControl.BaseLayer>
<LayersControl.Overlay checked name="Markers">
<LayerGroup>
<Marker position={[51.505, -0.09]} />
<Marker position={[51.51, -0.1]} />
</LayerGroup>
</LayersControl.Overlay>
</LayersControl>
<ZoomControl position="bottomright" />
<ScaleControl position="bottomleft" />
<AttributionControl position="bottomright" prefix={false} />
</MapContainer>
);
}function DynamicLayersControl({ layers }) {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<LayersControl>
{layers.baseLayers.map((layer) => (
<LayersControl.BaseLayer
key={layer.name}
name={layer.name}
checked={layer.default}
>
<TileLayer url={layer.url} attribution={layer.attribution} />
</LayersControl.BaseLayer>
))}
{layers.overlays.map((overlay) => (
<LayersControl.Overlay
key={overlay.name}
name={overlay.name}
checked={overlay.visible}
>
{overlay.component}
</LayersControl.Overlay>
))}
</LayersControl>
</MapContainer>
);
}function MapWithCustomScale() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<ScaleControl
position="bottomleft"
maxWidth={200}
metric={true}
imperial={false}
updateWhenIdle={false}
/>
</MapContainer>
);
}function MapWithGroupedOverlays() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<LayersControl position="topright" collapsed={false}>
<LayersControl.BaseLayer checked name="OpenStreetMap">
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</LayersControl.BaseLayer>
<LayersControl.Overlay checked name="Cities">
<FeatureGroup>
<Marker position={[51.505, -0.09]}>
<Popup>London</Popup>
</Marker>
<Marker position={[51.51, -0.1]}>
<Popup>Westminster</Popup>
</Marker>
</FeatureGroup>
</LayersControl.Overlay>
<LayersControl.Overlay name="Parks">
<FeatureGroup pathOptions={{ color: "green" }}>
<Circle center={[51.505, -0.09]} radius={200} />
<Circle center={[51.51, -0.1]} radius={150} />
</FeatureGroup>
</LayersControl.Overlay>
<LayersControl.Overlay name="Routes">
<Polyline
positions={[[51.505, -0.09], [51.51, -0.1], [51.52, -0.11]]}
pathOptions={{ color: "blue", weight: 3 }}
/>
</LayersControl.Overlay>
</LayersControl>
<ZoomControl position="topleft" />
<ScaleControl position="bottomleft" metric={true} imperial={true} />
</MapContainer>
);
}function MinimalMap() {
return (
<MapContainer
center={[51.505, -0.09]}
zoom={13}
zoomControl={false}
attributionControl={false}
style={{ height: "100vh" }}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{/* Only zoom control, hidden by default */}
<ZoomControl position="bottomright" />
</MapContainer>
);
}function MapWithMultiSourceAttribution() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13} attributionControl={false}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors'
/>
<AttributionControl
position="bottomright"
prefix='<a href="https://leafletjs.com/">Leaflet</a> | <a href="https://example.com">Custom Data</a>'
/>
</MapContainer>
);
}import { createControlComponent } from "@react-leaflet/core";
import L from "leaflet";
const InfoControl = L.Control.extend({
onAdd: function(map) {
const container = L.DomUtil.create("div", "info-control");
container.innerHTML = "<h4>Map Information</h4><p>Hover over features</p>";
container.style.background = "white";
container.style.padding = "10px";
container.style.borderRadius = "5px";
return container;
},
});
const InfoControlComponent = createControlComponent((props) => new InfoControl(props));
// Usage
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<InfoControlComponent position="topright" />
</MapContainer>function CustomButtonControl() {
const map = useMap();
useEffect(() => {
const CustomControl = L.Control.extend({
onAdd: function() {
const container = L.DomUtil.create("div", "leaflet-bar leaflet-control");
const button = L.DomUtil.create("a", "custom-button", container);
button.innerHTML = "🏠";
button.href = "#";
button.title = "Go Home";
button.style.width = "30px";
button.style.height = "30px";
button.style.lineHeight = "30px";
button.style.display = "block";
button.style.textAlign = "center";
L.DomEvent.on(button, "click", (e) => {
L.DomEvent.stopPropagation(e);
L.DomEvent.preventDefault(e);
map.setView([51.505, -0.09], 13);
});
return container;
},
});
const control = new CustomControl({ position: "topleft" });
control.addTo(map);
return () => {
control.remove();
};
}, [map]);
return null;
}function LegendControl() {
const map = useMap();
useEffect(() => {
const legend = L.control({ position: "bottomleft" });
legend.onAdd = function() {
const div = L.DomUtil.create("div", "legend");
div.style.background = "white";
div.style.padding = "10px";
div.style.borderRadius = "5px";
const grades = [0, 10, 20, 50, 100, 200, 500, 1000];
const colors = ["#FFEDA0", "#FED976", "#FEB24C", "#FD8D3C", "#FC4E2A", "#E31A1C", "#BD0026", "#800026"];
div.innerHTML = "<h4>Legend</h4>";
for (let i = 0; i < grades.length; i++) {
div.innerHTML +=
'<i style="background:' + colors[i] + '; width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7;"></i> ' +
grades[i] + (grades[i + 1] ? "–" + grades[i + 1] + "<br>" : "+");
}
return div;
};
legend.addTo(map);
return () => {
legend.remove();
};
}, [map]);
return null;
}function DynamicInfoControl({ data }) {
const map = useMap();
const controlRef = useRef(null);
useEffect(() => {
if (!controlRef.current) {
const InfoControl = L.Control.extend({
onAdd: function() {
const container = L.DomUtil.create("div", "info-control");
container.style.background = "white";
container.style.padding = "10px";
container.style.borderRadius = "5px";
controlRef.current = container;
return container;
},
});
const control = new InfoControl({ position: "topright" });
control.addTo(map);
}
if (controlRef.current) {
controlRef.current.innerHTML = `<h4>${data.title}</h4><p>${data.description}</p>`;
}
}, [map, data]);
return null;
}function SearchControl({ onSearch }) {
const map = useMap();
useEffect(() => {
const SearchControl = L.Control.extend({
onAdd: function() {
const container = L.DomUtil.create("div", "leaflet-bar search-control");
const input = L.DomUtil.create("input", "", container);
input.type = "text";
input.placeholder = "Search...";
input.style.padding = "5px";
L.DomEvent.on(input, "keypress", (e) => {
if (e.keyCode === 13) {
onSearch(input.value);
}
});
L.DomEvent.disableClickPropagation(container);
L.DomEvent.disableScrollPropagation(container);
return container;
},
});
const control = new SearchControl({ position: "topleft" });
control.addTo(map);
return () => {
control.remove();
};
}, [map, onSearch]);
return null;
}Solutions:
Solutions:
Solutions:
L.DomEvent.disableClickPropagation()L.DomEvent.stopPropagation() in event handlersL.DomEvent.preventDefault()import { render } from '@testing-library/react';
import { MapContainer, ZoomControl } from 'react-leaflet';
test('renders zoom control', () => {
const { container } = render(
<MapContainer center={[51.505, -0.09]} zoom={13} zoomControl={false}>
<ZoomControl position="bottomright" />
</MapContainer>
);
expect(container).toBeInTheDocument();
});test('renders layers control with base layers', () => {
render(
<MapContainer center={[51.505, -0.09]} zoom={13}>
<LayersControl position="topright">
<LayersControl.BaseLayer checked name="OpenStreetMap">
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
</LayersControl.BaseLayer>
</LayersControl>
</MapContainer>
);
// Add assertions
});<MapContainer
center={[51.505, -0.09]}
zoom={13}
keyboard={true}
zoomControl={false}
>
<ZoomControl
position="topright"
zoomInTitle="Zoom in (Keyboard: +)"
zoomOutTitle="Zoom out (Keyboard: -)"
/>
</MapContainer>function AccessibleCustomControl() {
const map = useMap();
useEffect(() => {
const CustomControl = L.Control.extend({
onAdd: function() {
const container = L.DomUtil.create("div", "custom-control");
const button = L.DomUtil.create("button", "", container);
button.innerHTML = "Reset View";
button.setAttribute("aria-label", "Reset map to default view");
button.setAttribute("role", "button");
L.DomEvent.on(button, "click", () => {
map.setView([51.505, -0.09], 13);
});
return container;
},
});
const control = new CustomControl({ position: "topleft" });
control.addTo(map);
return () => {
control.remove();
};
}, [map]);
return null;
}