tessl install tessl/npm-react-leaflet@5.0.3React components for Leaflet maps
Components for displaying images, SVG, and video content overlaid on specific geographic bounds.
Displays an image over specific geographic bounds.
/**
* Component for overlaying images on the map
* @param props - Image overlay properties
*/
const ImageOverlay: FunctionComponent<ImageOverlayProps>;
interface ImageOverlayProps extends MediaOverlayProps {
/** Image URL (required) */
url: string;
/** Child components (Popup, Tooltip) */
children?: ReactNode;
}Usage Example:
import { MapContainer, TileLayer, ImageOverlay } from "react-leaflet";
function Map() {
const bounds = [[51.49, -0.08], [51.5, -0.06]];
return (
<MapContainer center={[51.495, -0.07]} zoom={15}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<ImageOverlay
url="https://example.com/overlay-image.png"
bounds={bounds}
opacity={0.7}
/>
</MapContainer>
);
}With Popup:
<ImageOverlay
url="https://example.com/map-overlay.png"
bounds={[[51.49, -0.08], [51.5, -0.06]]}
opacity={0.8}
>
<Popup>This is an image overlay</Popup>
</ImageOverlay>Displays SVG content over specific geographic bounds.
/**
* Component for overlaying SVG on the map
* @param props - SVG overlay properties
*/
const SVGOverlay: FunctionComponent<SVGOverlayProps>;
interface SVGOverlayProps extends MediaOverlayProps {
/** SVG attributes */
attributes?: Record<string, string>;
/** SVG content as children */
children?: ReactNode;
}Usage Example:
import { MapContainer, TileLayer, SVGOverlay } from "react-leaflet";
function Map() {
const bounds = [[51.49, -0.08], [51.5, -0.06]];
return (
<MapContainer center={[51.495, -0.07]} zoom={15}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<SVGOverlay bounds={bounds}>
<rect x="0" y="0" width="100%" height="100%" fill="blue" opacity="0.3" />
<circle cx="50%" cy="50%" r="40%" fill="red" opacity="0.5" />
<text x="50%" y="50%" textAnchor="middle" fill="white">
Overlay Text
</text>
</SVGOverlay>
</MapContainer>
);
}With Custom Attributes:
<SVGOverlay
bounds={bounds}
attributes={{ preserveAspectRatio: "none" }}
>
<polygon points="0,0 100,0 100,100 0,100" fill="green" opacity="0.4" />
</SVGOverlay>Displays video content over specific geographic bounds.
/**
* Component for overlaying video on the map
* @param props - Video overlay properties
*/
const VideoOverlay: FunctionComponent<VideoOverlayProps>;
interface VideoOverlayProps extends MediaOverlayProps, VideoOverlayOptions {
/** Video source URL(s) or HTMLVideoElement (required) */
url: string | string[] | HTMLVideoElement;
/** Whether to play the video */
play?: boolean;
/** Child components (Popup, Tooltip) */
children?: ReactNode;
}Usage Example:
import { MapContainer, TileLayer, VideoOverlay } from "react-leaflet";
import { useState } from "react";
function Map() {
const [playing, setPlaying] = useState(false);
const bounds = [[51.49, -0.08], [51.5, -0.06]];
return (
<>
<button onClick={() => setPlaying(!playing)}>
{playing ? "Pause" : "Play"}
</button>
<MapContainer center={[51.495, -0.07]} zoom={15}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<VideoOverlay
url="https://example.com/video.mp4"
bounds={bounds}
play={playing}
/>
</MapContainer>
</>
);
}Multiple Video Sources:
<VideoOverlay
url={[
"https://example.com/video.mp4",
"https://example.com/video.webm",
"https://example.com/video.ogg",
]}
bounds={bounds}
play={true}
/>interface MediaOverlayProps extends ImageOverlayOptions, InteractiveLayerProps {
/** Geographic bounds of the overlay (required) */
bounds: LatLngBoundsExpression;
}
interface InteractiveLayerProps extends LayerProps, InteractiveLayerOptions {
interactive?: boolean;
bubblingMouseEvents?: boolean;
}// From Leaflet
interface ImageOverlayOptions extends InteractiveLayerOptions {
/** Opacity of the overlay (0.0 - 1.0) */
opacity?: number;
/** Alt text for the image */
alt?: string;
/** Whether image should be clipped to map bounds */
interactive?: boolean;
/** Crossorigin attribute for the image */
crossOrigin?: boolean | string;
/** Error image URL shown when image fails to load */
errorOverlayUrl?: string;
/** Z-index of the overlay */
zIndex?: number;
/** CSS class name */
className?: string;
}// From Leaflet
interface VideoOverlayOptions extends ImageOverlayOptions {
/** Whether video should autoplay */
autoplay?: boolean;
/** Whether video should loop */
loop?: boolean;
/** Whether to keep aspect ratio */
keepAspectRatio?: boolean;
/** Whether video should be muted */
muted?: boolean;
/** Whether to show video controls */
playsInline?: boolean;
}// From Leaflet
type LatLngBoundsExpression =
| LatLngBounds
| [LatLngExpression, LatLngExpression];
type LatLngExpression =
| LatLng
| [number, number]
| [number, number, number]
| { lat: number; lng: number }
| { lat: number; lng: number; alt?: number };
// Bounds are specified as [[southWest], [northEast]]
// Example: [[51.49, -0.12], [51.52, -0.06]]Bounds Specification: All overlays require geographic bounds defining where the content is positioned:
const bounds = [[southLat, westLng], [northLat, eastLng]];Opacity Control: Adjust overlay transparency:
<ImageOverlay url="..." bounds={bounds} opacity={0.5} />Z-Index: Control layering with zIndex:
<ImageOverlay url="base.png" bounds={bounds} zIndex={1} />
<ImageOverlay url="overlay.png" bounds={bounds} zIndex={2} />Video Playback: Control video with the play prop:
<VideoOverlay url="video.mp4" bounds={bounds} play={isPlaying} />Video Options: Configure video behavior:
<VideoOverlay
url="video.mp4"
bounds={bounds}
autoplay={false}
loop={true}
muted={false}
/>SVG Content: SVG children should be valid SVG elements:
<SVGOverlay bounds={bounds}>
<circle cx="50%" cy="50%" r="50%" fill="blue" />
</SVGOverlay>Interactive Overlays: Make overlays respond to mouse events:
<ImageOverlay
url="..."
bounds={bounds}
interactive={true}
eventHandlers={{
click: () => console.log("Overlay clicked"),
}}
/>CORS Considerations: For cross-origin images, set crossOrigin:
<ImageOverlay url="..." bounds={bounds} crossOrigin={true} />Dynamic Bounds: Update overlay position by changing bounds:
<ImageOverlay url="..." bounds={currentBounds} />Error Handling: Provide fallback image for failed loads:
<ImageOverlay
url="primary.png"
bounds={bounds}
errorOverlayUrl="fallback.png"
/>function ImageOverlayWithControls() {
const [opacity, setOpacity] = useState(0.7);
const bounds = [[51.49, -0.08], [51.5, -0.06]];
return (
<>
<input
type="range"
min="0"
max="1"
step="0.1"
value={opacity}
onChange={(e) => setOpacity(parseFloat(e.target.value))}
/>
<MapContainer center={[51.495, -0.07]} zoom={15}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<ImageOverlay
url="https://example.com/overlay.png"
bounds={bounds}
opacity={opacity}
/>
</MapContainer>
</>
);
}function DynamicSVGOverlay({ temperature }) {
const bounds = [[51.49, -0.08], [51.5, -0.06]];
const color = temperature > 30 ? "red" : temperature > 20 ? "orange" : "blue";
return (
<SVGOverlay bounds={bounds}>
<rect x="0" y="0" width="100%" height="100%" fill={color} opacity="0.3" />
<text x="50%" y="50%" textAnchor="middle" fill="white" fontSize="24">
{temperature}°C
</text>
</SVGOverlay>
);
}function VideoOverlayWithControls() {
const [playing, setPlaying] = useState(false);
const [loop, setLoop] = useState(true);
const bounds = [[51.49, -0.08], [51.5, -0.06]];
return (
<>
<div>
<button onClick={() => setPlaying(!playing)}>
{playing ? "Pause" : "Play"}
</button>
<label>
<input
type="checkbox"
checked={loop}
onChange={(e) => setLoop(e.target.checked)}
/>
Loop
</label>
</div>
<MapContainer center={[51.495, -0.07]} zoom={15}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<VideoOverlay
url="https://example.com/video.mp4"
bounds={bounds}
play={playing}
loop={loop}
muted={true}
>
<Popup>
<button onClick={() => setPlaying(!playing)}>
Toggle Playback
</button>
</Popup>
</VideoOverlay>
</MapContainer>
</>
);
}function HistoricalMapOverlay() {
const [showHistorical, setShowHistorical] = useState(false);
const [opacity, setOpacity] = useState(0.7);
const bounds = [[51.49, -0.12], [51.52, -0.06]];
return (
<>
<button onClick={() => setShowHistorical(!showHistorical)}>
Toggle Historical Map
</button>
<MapContainer center={[51.505, -0.09]} zoom={14}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{showHistorical && (
<ImageOverlay
url="https://example.com/historical-map.png"
bounds={bounds}
opacity={opacity}
>
<Popup>Historical map from 1900</Popup>
</ImageOverlay>
)}
</MapContainer>
</>
);
}function InteractiveSVGOverlay() {
const [clicked, setClicked] = useState(false);
const bounds = [[51.49, -0.08], [51.5, -0.06]];
return (
<SVGOverlay
bounds={bounds}
interactive={true}
eventHandlers={{
click: () => setClicked(!clicked),
}}
>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill={clicked ? "green" : "red"}
opacity="0.4"
/>
<text x="50%" y="50%" textAnchor="middle" fill="white">
{clicked ? "Clicked!" : "Click me"}
</text>
</SVGOverlay>
);
}function DynamicBoundsOverlay({ center, size }) {
const bounds = useMemo(() => {
const offset = size / 2;
return [
[center[0] - offset, center[1] - offset],
[center[0] + offset, center[1] + offset],
];
}, [center, size]);
return (
<ImageOverlay
url="https://example.com/overlay.png"
bounds={bounds}
opacity={0.7}
/>
);
}function RotatingImageOverlay({ bounds, angle }) {
const overlayRef = useRef(null);
useEffect(() => {
if (overlayRef.current) {
const element = overlayRef.current.getElement();
if (element) {
element.style.transform = `rotate(${angle}deg)`;
element.style.transformOrigin = "center";
}
}
}, [angle]);
return (
<ImageOverlay
ref={overlayRef}
url="https://example.com/overlay.png"
bounds={bounds}
/>
);
}function ControlledVideoOverlay() {
const [playing, setPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const videoRef = useRef(null);
const bounds = [[51.49, -0.08], [51.5, -0.06]];
useEffect(() => {
if (videoRef.current) {
const video = videoRef.current.getElement();
if (video) {
video.addEventListener('timeupdate', () => {
setCurrentTime(video.currentTime);
});
}
}
}, []);
return (
<>
<div className="video-controls">
<button onClick={() => setPlaying(!playing)}>
{playing ? "Pause" : "Play"}
</button>
<span>Time: {currentTime.toFixed(1)}s</span>
</div>
<VideoOverlay
ref={videoRef}
url="https://example.com/video.mp4"
bounds={bounds}
play={playing}
loop={true}
muted={true}
/>
</>
);
}function DataVisualizationOverlay({ data, bounds }) {
const maxValue = Math.max(...data.map(d => d.value));
return (
<SVGOverlay bounds={bounds}>
<defs>
<linearGradient id="heatGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style={{ stopColor: "blue", stopOpacity: 1 }} />
<stop offset="50%" style={{ stopColor: "yellow", stopOpacity: 1 }} />
<stop offset="100%" style={{ stopColor: "red", stopOpacity: 1 }} />
</linearGradient>
</defs>
{data.map((point, idx) => (
<circle
key={idx}
cx={`${(point.x / 100) * 100}%`}
cy={`${(point.y / 100) * 100}%`}
r={`${(point.value / maxValue) * 10}%`}
fill="url(#heatGradient)"
opacity="0.6"
/>
))}
</SVGOverlay>
);
}function RobustImageOverlay({ urls, bounds }) {
const [currentUrlIndex, setCurrentUrlIndex] = useState(0);
const [error, setError] = useState(false);
const handleError = useCallback(() => {
if (currentUrlIndex < urls.length - 1) {
setCurrentUrlIndex(prev => prev + 1);
setError(false);
} else {
setError(true);
}
}, [currentUrlIndex, urls.length]);
if (error) {
return <div>Failed to load overlay image</div>;
}
return (
<ImageOverlay
url={urls[currentUrlIndex]}
bounds={bounds}
eventHandlers={{
error: handleError,
}}
/>
);
}function AnimatedSVGOverlay({ bounds }) {
const [frame, setFrame] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setFrame(prev => (prev + 1) % 360);
}, 50);
return () => clearInterval(interval);
}, []);
return (
<SVGOverlay bounds={bounds}>
<circle
cx="50%"
cy="50%"
r="20%"
fill="blue"
opacity="0.5"
transform={`rotate(${frame} 50 50)`}
/>
<line
x1="50%"
y1="50%"
x2={`${50 + 30 * Math.cos(frame * Math.PI / 180)}%`}
y2={`${50 + 30 * Math.sin(frame * Math.PI / 180)}%`}
stroke="red"
strokeWidth="2"
/>
</SVGOverlay>
);
}function ZoomDependentOverlay() {
const [zoom, setZoom] = useState(13);
const bounds = [[51.49, -0.08], [51.5, -0.06]];
return (
<MapContainer center={[51.495, -0.07]} zoom={zoom}>
<MapEvents onZoomChange={setZoom} />
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
{zoom > 14 && (
<ImageOverlay
url="https://example.com/detailed-overlay.png"
bounds={bounds}
opacity={0.7}
/>
)}
{zoom <= 14 && (
<ImageOverlay
url="https://example.com/simple-overlay.png"
bounds={bounds}
opacity={0.5}
/>
)}
</MapContainer>
);
}Solutions:
Solutions:
Solutions:
play prop is set to trueautoplay and muted settings (autoplay requires muted)Solutions:
Solutions:
Solutions:
crossOrigin={true} on overlayimport { render } from '@testing-library/react';
import { MapContainer, ImageOverlay } from 'react-leaflet';
test('renders image overlay', () => {
const bounds = [[51.49, -0.08], [51.5, -0.06]];
const { container } = render(
<MapContainer center={[51.495, -0.07]} zoom={15}>
<ImageOverlay
url="https://example.com/image.png"
bounds={bounds}
opacity={0.7}
/>
</MapContainer>
);
expect(container).toBeInTheDocument();
});test('video overlay responds to play prop', () => {
const bounds = [[51.49, -0.08], [51.5, -0.06]];
const { rerender } = render(
<MapContainer center={[51.495, -0.07]} zoom={15}>
<VideoOverlay
url="https://example.com/video.mp4"
bounds={bounds}
play={false}
/>
</MapContainer>
);
// Test play state change
rerender(
<MapContainer center={[51.495, -0.07]} zoom={15}>
<VideoOverlay
url="https://example.com/video.mp4"
bounds={bounds}
play={true}
/>
</MapContainer>
);
});<ImageOverlay
url="https://example.com/map-overlay.png"
bounds={bounds}
alt="Historical map overlay from 1900"
>
<Popup>
<div role="region" aria-label="Historical map information">
<h3>Historical Map</h3>
<p>This overlay shows the area as it appeared in 1900</p>
</div>
</Popup>
</ImageOverlay><VideoOverlay
url="https://example.com/video.mp4"
bounds={bounds}
play={playing}
muted={false}
// Ensure video is accessible
>
<Popup>
<div role="region">
<h3>Video Content</h3>
<button
onClick={() => setPlaying(!playing)}
aria-label={playing ? "Pause video" : "Play video"}
>
{playing ? "Pause" : "Play"}
</button>
</div>
</Popup>
</VideoOverlay>