tessl install tessl/npm-react-leaflet@5.0.3React components for Leaflet maps
Components for placing markers on the map with optional popups and tooltips for displaying information.
Places an icon marker at a specific geographic location.
/**
* Component for displaying an icon marker on the map
* @param props - Marker properties
*/
const Marker: FunctionComponent<MarkerProps>;
interface MarkerProps extends MarkerOptions, EventedProps {
/** Geographic position of the marker (required) */
position: LatLngExpression;
/** Child components (Popup, Tooltip) */
children?: ReactNode;
}Usage Example:
import { MapContainer, TileLayer, Marker, Popup } 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" />
<Marker position={[51.505, -0.09]}>
<Popup>
A marker with a popup.
</Popup>
</Marker>
</MapContainer>
);
}Multiple Markers:
const locations = [
{ id: 1, position: [51.505, -0.09], title: "Location 1" },
{ id: 2, position: [51.51, -0.1], title: "Location 2" },
{ id: 3, position: [51.49, -0.08], title: "Location 3" },
];
function Map() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{locations.map((loc) => (
<Marker key={loc.id} position={loc.position}>
<Popup>{loc.title}</Popup>
</Marker>
))}
</MapContainer>
);
}Information popup that can be attached to markers or displayed standalone.
/**
* Component for displaying popup overlays
* @param props - Popup properties
*/
const Popup: FunctionComponent<PopupProps>;
interface PopupProps extends PopupOptions, EventedProps {
/** Popup content */
children?: ReactNode;
/** Position for standalone popup (not attached to marker) */
position?: LatLngExpression;
}Popup Attached to Marker:
<Marker position={[51.505, -0.09]}>
<Popup>
<h3>Marker Title</h3>
<p>Marker description</p>
</Popup>
</Marker>Standalone Popup:
<Popup position={[51.505, -0.09]}>
<div>
<h3>Standalone Popup</h3>
<p>This popup is not attached to a marker</p>
</div>
</Popup>Small informational tooltip that appears on hover or can be permanent.
/**
* Component for displaying tooltip overlays
* @param props - Tooltip properties
*/
const Tooltip: FunctionComponent<TooltipProps>;
interface TooltipProps extends TooltipOptions, EventedProps {
/** Tooltip content */
children?: ReactNode;
/** Position for standalone tooltip */
position?: LatLngExpression;
}Tooltip on Marker:
<Marker position={[51.505, -0.09]}>
<Tooltip>Hover tooltip text</Tooltip>
</Marker>Permanent Tooltip:
<Marker position={[51.505, -0.09]}>
<Tooltip permanent>Always visible label</Tooltip>
</Marker>Tooltip with Popup:
<Marker position={[51.505, -0.09]}>
<Popup>Detailed information</Popup>
<Tooltip>Quick label</Tooltip>
</Marker>// From Leaflet
interface MarkerOptions extends InteractiveLayerOptions {
/** Icon instance to use for the marker */
icon?: Icon | DivIcon;
/** Whether the marker is keyboard accessible */
keyboard?: boolean;
/** Title attribute for the marker */
title?: string;
/** Alt text for the marker image */
alt?: string;
/** z-index offset for the marker */
zIndexOffset?: number;
/** Marker opacity (0.0 - 1.0) */
opacity?: number;
/** Whether the marker can be moved by dragging */
draggable?: boolean;
/** Whether to show the marker on top while dragging */
autoPan?: boolean;
/** Options for auto panning */
autoPanPadding?: Point;
/** Speed of auto panning in pixels */
autoPanSpeed?: number;
/** Whether to bring marker to front on mouse over */
riseOnHover?: boolean;
/** z-index offset when hovered */
riseOffset?: number;
/** Pane where the marker will be added */
pane?: string;
/** Attribution text */
attribution?: string;
}
interface InteractiveLayerOptions extends LayerOptions {
/** Whether layer responds to mouse events */
interactive?: boolean;
/** Whether mouse events bubble to map */
bubblingMouseEvents?: boolean;
}// From Leaflet
interface PopupOptions extends DivOverlayOptions {
/** Maximum width of the popup in pixels */
maxWidth?: number;
/** Minimum width of the popup in pixels */
minWidth?: number;
/** Maximum height; sets scrollable container if exceeded */
maxHeight?: number;
/** Whether to auto pan on popup open */
autoPan?: boolean;
/** Margin between popup and map edge */
autoPanPaddingTopLeft?: Point;
/** Margin between popup and map edge */
autoPanPaddingBottomRight?: Point;
/** Padding of auto pan */
autoPanPadding?: Point;
/** Keep popup in view during pan/zoom */
keepInView?: boolean;
/** Whether to show close button */
closeButton?: boolean;
/** Whether to automatically close previous popup */
autoClose?: boolean;
/** Whether to close on Escape key */
closeOnEscapeKey?: boolean;
/** Whether to close when clicking the map */
closeOnClick?: boolean;
/** CSS class name for the popup */
className?: string;
}
interface DivOverlayOptions {
/** Offset of the overlay position */
offset?: Point;
/** CSS class name */
className?: string;
/** Map pane where the overlay will be added */
pane?: string;
}
type Point = [number, number] | { x: number; y: number };// From Leaflet
interface TooltipOptions extends DivOverlayOptions {
/** Direction where to open the tooltip relative to source */
direction?: 'right' | 'left' | 'top' | 'bottom' | 'center' | 'auto';
/** Whether tooltip is permanent (always visible) */
permanent?: boolean;
/** Whether tooltip stays open on mouse out */
sticky?: boolean;
/** Opacity of the tooltip */
opacity?: number;
/** Offset of tooltip position */
offset?: Point;
/** CSS class name for the tooltip */
className?: string;
}import { Icon } from "leaflet";
const customIcon = new Icon({
iconUrl: "/marker-icon.png",
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowUrl: "/marker-shadow.png",
shadowSize: [41, 41],
});
<Marker position={[51.505, -0.09]} icon={customIcon}>
<Popup>Marker with custom icon</Popup>
</Marker>import { DivIcon } from "leaflet";
const divIcon = new DivIcon({
html: '<div class="custom-marker">📍</div>',
className: "custom-div-icon",
iconSize: [30, 30],
iconAnchor: [15, 30],
});
<Marker position={[51.505, -0.09]} icon={divIcon} />const svgIcon = new DivIcon({
html: `
<svg width="30" height="40" viewBox="0 0 30 40">
<path d="M15,0 C6.7,0 0,6.7 0,15 C0,26.3 15,40 15,40 S30,26.3 30,15 C30,6.7 23.3,0 15,0 Z"
fill="#ff0000" stroke="#fff" stroke-width="2"/>
<circle cx="15" cy="15" r="5" fill="#fff"/>
</svg>
`,
className: "",
iconSize: [30, 40],
iconAnchor: [15, 40],
});
<Marker position={[51.505, -0.09]} icon={svgIcon} />Position Updates: The marker position can be updated dynamically by changing the position prop:
<Marker position={currentPosition} />Draggable Markers: Enable marker dragging with event handler:
<Marker
position={position}
draggable={true}
eventHandlers={{
dragend: (e) => {
const newPos = e.target.getLatLng();
console.log("New position:", newPos);
},
}}
/>Popup Control: Popups automatically open when marker is clicked by default. Control this behavior:
<Popup autoClose={false} closeOnClick={false}>
Always open popup
</Popup>Tooltip Directions: Tooltips can open in different directions:
<Tooltip direction="top">Appears above marker</Tooltip>
<Tooltip direction="right">Appears to the right</Tooltip>Event Handling: Attach event handlers to markers:
<Marker
position={[51.505, -0.09]}
eventHandlers={{
click: (e) => console.log("Marker clicked", e),
mouseover: (e) => console.log("Mouse over marker"),
dragstart: (e) => console.log("Drag started"),
dragend: (e) => console.log("Drag ended", e.target.getLatLng()),
}}
/>Z-Index: Control marker stacking with zIndexOffset:
<Marker position={pos1} zIndexOffset={1000} />
<Marker position={pos2} zIndexOffset={2000} />Opacity: Adjust marker transparency:
<Marker position={[51.505, -0.09]} opacity={0.5} />Keyboard Access: Markers are keyboard accessible by default:
<Marker position={[51.505, -0.09]} keyboard={true} />Default Icon Fix: In bundled applications, fix default icon paths:
import L from "leaflet";
import icon from "leaflet/dist/images/marker-icon.png";
import iconShadow from "leaflet/dist/images/marker-shadow.png";
let DefaultIcon = L.icon({
iconUrl: icon,
shadowUrl: iconShadow,
iconSize: [25, 41],
iconAnchor: [12, 41],
});
L.Marker.prototype.options.icon = DefaultIcon;Popup Content: Popups can contain any React content including interactive elements:
<Popup>
<div>
<h3>Interactive Popup</h3>
<button onClick={() => alert('Clicked!')}>Click me</button>
</div>
</Popup>function InteractiveMarker() {
const [position, setPosition] = useState([51.505, -0.09]);
return (
<Marker
position={position}
draggable={true}
eventHandlers={{
dragend: (e) => {
const newPos = e.target.getLatLng();
setPosition([newPos.lat, newPos.lng]);
},
}}
>
<Popup>
Position: {position[0].toFixed(4)}, {position[1].toFixed(4)}
</Popup>
</Marker>
);
}import { Icon } from "leaflet";
const icon = new Icon({
iconUrl: "/custom-marker.png",
iconSize: [32, 32],
iconAnchor: [16, 32],
});
<Marker position={[51.505, -0.09]} icon={icon}>
<Tooltip permanent direction="top">
Important Location
</Tooltip>
<Popup>
<h3>Important Location</h3>
<p>More details here</p>
</Popup>
</Marker>function MarkerList({ locations }) {
return (
<>
{locations.map((loc, idx) => (
<Marker
key={loc.id}
position={loc.position}
eventHandlers={{
click: () => console.log(`Clicked marker ${loc.id}`),
}}
>
<Popup>
<div>
<h4>{loc.title}</h4>
<p>{loc.description}</p>
</div>
</Popup>
<Tooltip>{loc.title}</Tooltip>
</Marker>
))}
</>
);
}function MarkerWithRichPopup() {
const [likes, setLikes] = useState(0);
return (
<Marker position={[51.505, -0.09]}>
<Popup>
<div>
<h3>Interactive Popup</h3>
<p>Likes: {likes}</p>
<button onClick={() => setLikes(likes + 1)}>
Like
</button>
</div>
</Popup>
</Marker>
);
}function AnimatedMarker({ position }) {
const markerRef = useRef(null);
useEffect(() => {
if (markerRef.current) {
const marker = markerRef.current;
// Animate marker with CSS or Leaflet animations
marker.setOpacity(0);
setTimeout(() => marker.setOpacity(1), 100);
}
}, [position]);
return (
<Marker ref={markerRef} position={position}>
<Popup>Animated marker</Popup>
</Marker>
);
}function ConditionalMarkers({ markers, filter }) {
const filteredMarkers = markers.filter(m => m.category === filter);
return (
<>
{filteredMarkers.map(marker => (
<Marker key={marker.id} position={marker.position}>
<Popup>{marker.name}</Popup>
</Marker>
))}
</>
);
}<Marker position={[51.505, -0.09]}>
<Popup
className="custom-popup"
maxWidth={300}
minWidth={200}
closeButton={false}
autoPan={true}
>
<div style={{ padding: '10px', backgroundColor: '#f0f0f0' }}>
<h3 style={{ margin: 0 }}>Custom Styled Popup</h3>
<p>With custom styling</p>
</div>
</Popup>
</Marker>function DynamicIconMarker({ position, status }) {
const icon = useMemo(() => {
const color = status === 'active' ? 'green' : 'red';
return new DivIcon({
html: `<div style="background-color: ${color}; width: 20px; height: 20px; border-radius: 50%;"></div>`,
className: '',
iconSize: [20, 20],
});
}, [status]);
return (
<Marker position={position} icon={icon}>
<Popup>Status: {status}</Popup>
</Marker>
);
}function AsyncMarker({ id }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/markers/${id}`)
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false);
});
}, [id]);
if (loading || !data) return null;
return (
<Marker position={data.position}>
<Popup>
<h3>{data.title}</h3>
<p>{data.description}</p>
</Popup>
</Marker>
);
}function ClusteredMarkers({ markers, zoom }) {
const clustered = useMemo(() => {
if (zoom > 12) return markers; // Show all markers at high zoom
// Simple clustering logic
const clusters = new Map();
markers.forEach(marker => {
const key = `${Math.floor(marker.position[0])},${Math.floor(marker.position[1])}`;
if (!clusters.has(key)) {
clusters.set(key, []);
}
clusters.get(key).push(marker);
});
return Array.from(clusters.values()).map(group => ({
position: group[0].position,
count: group.length,
markers: group,
}));
}, [markers, zoom]);
return (
<>
{clustered.map((cluster, idx) => (
<Marker key={idx} position={cluster.position}>
<Popup>
{cluster.count} markers
{cluster.markers.map(m => (
<div key={m.id}>{m.title}</div>
))}
</Popup>
</Marker>
))}
</>
);
}function MarkerWithRef() {
const markerRef = useRef(null);
const handleClick = () => {
if (markerRef.current) {
markerRef.current.openPopup();
}
};
return (
<>
<button onClick={handleClick}>Open Marker Popup</button>
<Marker ref={markerRef} position={[51.505, -0.09]}>
<Popup>Controlled popup</Popup>
</Marker>
</>
);
}const MarkerComponent = React.memo(({ position, title }) => (
<Marker position={position}>
<Popup>{title}</Popup>
</Marker>
));
function OptimizedMarkerList({ markers }) {
return (
<>
{markers.map(marker => (
<MarkerComponent
key={marker.id}
position={marker.position}
title={marker.title}
/>
))}
</>
);
}function VirtualizedMarkers({ markers }) {
const map = useMap();
const [visibleMarkers, setVisibleMarkers] = useState([]);
useEffect(() => {
const updateVisibleMarkers = () => {
const bounds = map.getBounds();
const visible = markers.filter(m =>
bounds.contains(m.position)
);
setVisibleMarkers(visible);
};
updateVisibleMarkers();
map.on('moveend', updateVisibleMarkers);
map.on('zoomend', updateVisibleMarkers);
return () => {
map.off('moveend', updateVisibleMarkers);
map.off('zoomend', updateVisibleMarkers);
};
}, [map, markers]);
return (
<>
{visibleMarkers.map(marker => (
<Marker key={marker.id} position={marker.position}>
<Popup>{marker.title}</Popup>
</Marker>
))}
</>
);
}Solutions:
Solutions:
autoClose and closeOnClick settingsSolutions:
draggable={true} on Markerdragend event handlerSolutions:
import { render, screen } from '@testing-library/react';
import { MapContainer, Marker, Popup } from 'react-leaflet';
test('renders marker with popup', () => {
render(
<MapContainer center={[51.505, -0.09]} zoom={13}>
<Marker position={[51.505, -0.09]}>
<Popup>Test Popup</Popup>
</Marker>
</MapContainer>
);
// Add assertions based on your testing needs
});test('handles marker click events', () => {
const handleClick = jest.fn();
render(
<MapContainer center={[51.505, -0.09]} zoom={13}>
<Marker
position={[51.505, -0.09]}
eventHandlers={{ click: handleClick }}
/>
</MapContainer>
);
// Simulate click and verify handler called
});<Marker
position={[51.505, -0.09]}
keyboard={true}
title="Location marker - Press Enter to open details"
>
<Popup>
<div role="dialog" aria-label="Location details">
<h3>Location Name</h3>
<p>Additional information</p>
</div>
</Popup>
</Marker><Marker position={[51.505, -0.09]}>
<Popup>
<div aria-live="polite">
<span className="sr-only">Marker at coordinates {lat}, {lng}</span>
<h3>{locationName}</h3>
</div>
</Popup>
</Marker>