About us Guides Projects Contacts
Админка
please wait

Leaflet.js is the leading open-source JavaScript library for mobile-friendly interactive maps. Unlike Google Maps or Mapbox, Leaflet is lightweight (~42KB gzipped), works with any tile provider, and offers complete control over styling and behavior. This guide covers building production-ready mapping applications from a senior developer's perspective.

Why Leaflet

Leaflet offers compelling advantages:

  1. Lightweight: Only 42KB, loads fast everywhere
  2. Provider Agnostic: Works with OpenStreetMap, Mapbox, custom tiles
  3. Mobile-First: Touch-friendly, responsive out of the box
  4. Extensible: Rich plugin ecosystem for any feature
  5. Free: No API keys required for basic usage

Getting Started

Installation

<!--CSS-->
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
<!--JavaScript-->
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<!--Container-->
<div id="map" style="height: 500px;"></div>

Or with npm:

npm install leaflet
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';

Basic Map

// Initialize map centered on coordinates
const map = L.map('map').setView([51.505, -0.09], 13);
// Add OpenStreetMap tiles
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
}).addTo(map);
// Add a marker
L.marker([51.5, -0.09])
.addTo(map)
.bindPopup('Hello from London!')
.openPopup();

Tile Providers

OpenStreetMap (Free)

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);

Mapbox (Custom Styling)

L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: '© Mapbox © OpenStreetMap',
id: 'mapbox/streets-v11',
accessToken: 'your.mapbox.access.token'
}).addTo(map);

Satellite Imagery

L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© Esri, Maxar, Earthstar Geographics'
}).addTo(map);

Layer Switching

const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
});
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© Esri'
});
const map = L.map('map', {
center: [51.505, -0.09],
zoom: 13,
layers: [osm] // Default layer
});
L.control.layers({
'Street Map': osm,
'Satellite': satellite
}).addTo(map);

Markers and Popups

Basic Markers

// Simple marker
const marker = L.marker([51.5, -0.09]).addTo(map);
// With popup
marker.bindPopup('<b>Location</b><br>Description here');
// Open popup immediately
marker.openPopup();
// With tooltip (shows on hover)
marker.bindTooltip('Quick info');

Custom Icons

const customIcon = L.icon({
iconUrl: '/images/marker.png',
iconSize: [32, 32],
iconAnchor: [16, 32], // Point that corresponds to marker position
popupAnchor: [0, -32] // Point where popup opens relative to iconAnchor
});
L.marker([51.5, -0.09], { icon: customIcon }).addTo(map);

Marker Clusters

For many markers, use the MarkerCluster plugin:

<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.css" />
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>
const markers = L.markerClusterGroup();
locations.forEach(loc => {
const marker = L.marker([loc.lat, loc.lng])
.bindPopup(loc.name);
markers.addLayer(marker);
});
map.addLayer(markers);

Drawing Shapes

Circles and Polygons

// Circle (radius in meters)
L.circle([51.508, -0.11], {
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5,
radius: 500
}).addTo(map);
// Polygon
L.polygon([
[51.509, -0.08],
[51.503, -0.06],
[51.51, -0.047]
], {
color: 'blue',
fillOpacity: 0.3
}).addTo(map);
// Rectangle
L.rectangle([
[51.49, -0.08],
[51.5, -0.06]
], {
color: '#ff7800',
weight: 1
}).addTo(map);
// Polyline (no fill)
L.polyline([
[51.505, -0.09],
[51.51, -0.1],
[51.51, -0.12]
], {
color: 'green',
weight: 3,
dashArray: '5, 10'
}).addTo(map);

Working with GeoJSON

GeoJSON is the standard format for geographic data:

const geojsonData = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Central Park",
"category": "park"
},
"geometry": {
"type": "Point",
"coordinates": [-73.965355, 40.782865]
}
},
{
"type": "Feature",
"properties": {
"name": "Manhattan",
"population": 1628706
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[-74.047285, 40.679548],
[-73.907000, 40.679548],
[-73.907000, 40.882214],
[-74.047285, 40.882214],
[-74.047285, 40.679548]
]]
}
}
]
};
// Add GeoJSON with styling
L.geoJSON(geojsonData, {
style: function(feature) {
return {
color: feature.properties.category === 'park' ? 'green' : 'blue',
fillOpacity: 0.5
};
},
pointToLayer: function(feature, latlng) {
return L.circleMarker(latlng, {
radius: 8,
fillColor: '#ff7800',
color: '#000',
weight: 1,
fillOpacity: 0.8
});
},
onEachFeature: function(feature, layer) {
layer.bindPopup(`<b>${feature.properties.name}</b>`);
}
}).addTo(map);

Load GeoJSON from URL

fetch('/api/locations.geojson')
.then(response => response.json())
.then(data => {
L.geoJSON(data).addTo(map);
});

Map Events

Click Events

// Map click
map.on('click', function(e) {
console.log('Clicked at:', e.latlng);
L.popup()
.setLatLng(e.latlng)
.setContent(`Coordinates: ${e.latlng.lat.toFixed(4)}, ${e.latlng.lng.toFixed(4)}`)
.openOn(map);
});
// Marker click
marker.on('click', function(e) {
console.log('Marker clicked');
});

Map Movement

map.on('moveend', function() {
const center = map.getCenter();
const zoom = map.getZoom();
const bounds = map.getBounds();
console.log('Center:', center);
console.log('Zoom:', zoom);
console.log('Bounds:', bounds);
// Load data for visible area
loadMarkersInBounds(bounds);
});
map.on('zoomend', function() {
const zoom = map.getZoom();
// Show/hide layers based on zoom
if (zoom > 15) {
detailLayer.addTo(map);
} else {
map.removeLayer(detailLayer);
}
});

User Location

// Locate user
map.locate({ setView: true, maxZoom: 16 });
map.on('locationfound', function(e) {
const radius = e.accuracy / 2;
L.marker(e.latlng)
.addTo(map)
.bindPopup(`You are within ${radius} meters`)
.openPopup();
L.circle(e.latlng, { radius: radius }).addTo(map);
});
map.on('locationerror', function(e) {
alert('Location access denied');
});

Custom Controls

Create Custom Control

const InfoControl = L.Control.extend({
options: {
position: 'bottomright'
},
onAdd: function(map) {
const container = L.DomUtil.create('div', 'info-control');
container.innerHTML = '<h4>Map Info</h4><div id="info-content"></div>';
// Prevent map interactions when clicking the control
L.DomEvent.disableClickPropagation(container);
return container;
},
update: function(content) {
document.getElementById('info-content').innerHTML = content;
}
});
const infoControl = new InfoControl();
map.addControl(infoControl);
// Update on feature hover
layer.on('mouseover', function(e) {
infoControl.update(`<b>${e.target.feature.properties.name}</b>`);
});

Vue.js Integration

<template>
<div id="map" ref="mapContainer"></div>
</template>
<script>
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
export default {
props: {
center: {
type: Array,
default: () => [51.505, -0.09]
},
markers: {
type: Array,
default: () => []
}
},
data() {
return {
map: null,
markerLayer: null
};
},
mounted() {
this.initMap();
},
watch: {
markers: {
handler(newMarkers) {
this.updateMarkers(newMarkers);
},
deep: true
}
},
methods: {
initMap() {
this.map = L.map(this.$refs.mapContainer).setView(this.center, 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(this.map);
this.markerLayer = L.layerGroup().addTo(this.map);
this.updateMarkers(this.markers);
},
updateMarkers(markers) {
this.markerLayer.clearLayers();
markers.forEach(m => {
L.marker([m.lat, m.lng])
.bindPopup(m.popup)
.addTo(this.markerLayer);
});
}
},
beforeDestroy() {
if (this.map) {
this.map.remove();
}
}
};
</script>
<style>
#map {
height: 400px;
width: 100%;
}
</style>

Performance Tips

Large Datasets

// Use Canvas renderer for thousands of markers
const map = L.map('map', {
renderer: L.canvas()
});
// Or per layer
L.circleMarker([lat, lng], {
renderer: L.canvas()
}).addTo(map);
// Simplify GeoJSON on zoom
L.geoJSON(complexData, {
style: { weight: 2 },
smoothFactor: 1.5 // Simplify paths
});

Lazy Loading

// Load markers only in visible bounds
map.on('moveend', debounce(async function() {
const bounds = map.getBounds();
const response = await fetch(`/api/markers?bbox=${bounds.toBBoxString()}`);
const data = await response.json();
markerLayer.clearLayers();
L.geoJSON(data).addTo(markerLayer);
}, 300));

Key Takeaways

  1. Choose the right tile provider: OSM is free, Mapbox for custom styles
  2. Cluster markers: Essential for datasets over ~100 markers
  3. Use GeoJSON: Standard format works everywhere
  4. Handle events properly: Debounce expensive operations
  5. Canvas for performance: Switch renderer for large datasets
  6. Mobile-first: Test touch interactions thoroughly

Leaflet provides everything needed for professional mapping applications while remaining lightweight and flexible—perfect for projects that need maps without vendor lock-in.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive