Building Your First Interactive Web Map with Leaflet.js

Maps have always been one of the most powerful ways to communicate spatial information. Whether you are visualizing store locations, tracking delivery routes, or building a field data collection tool, an interactive web map transforms raw coordinates into something people can actually explore and understand. Leaflet.js makes this surprisingly accessible — it is lightweight, open-source, and does not require a geography degree to get started.

This article walks you through building your first interactive web map from scratch, covering everything from setup to adding custom markers and popups.


What is Leaflet.js?

Leaflet is an open-source JavaScript library for mobile-friendly interactive maps. Released in 2011 by Vladimir Agafonkin, it has become one of the most widely used mapping libraries on the web. At around 42KB gzipped, it delivers most of what you need for standard mapping tasks without the bloat of heavier alternatives like Google Maps API or OpenLayers.

Leaflet handles the rendering, user interaction (pan, zoom, click), and layer management. It does not come with its own map tiles — for that, you pull in a tile provider like OpenStreetMap, Mapbox, or Esri.


Prerequisites

You do not need a backend or a build system to follow this tutorial. All you need is:

  • A text editor (VS Code, Sublime, anything works)
  • A modern web browser
  • Basic familiarity with HTML, CSS, and JavaScript

Step 1: Set Up Your HTML File

Create a new file called index.html and add the following boilerplate:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>My First Leaflet Map</title>

  <!-- Leaflet CSS -->
  <link
    rel="stylesheet"
    href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
  />

  <style>
    body {
      margin: 0;
      padding: 0;
    }

    #map {
      width: 100vw;
      height: 100vh;
    }
  </style>
</head>
<body>

  <div id="map"></div>

  <!-- Leaflet JS -->
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
  <script src="app.js"></script>

</body>
</html>

A few things to note here. The #map div is where your map will render — it must have a defined height, otherwise Leaflet will render an empty container. Using 100vh fills the entire viewport. The Leaflet CSS is required for the map controls, popups, and tooltips to render correctly, so do not skip it.


Step 2: Initialize the Map

Create a new file called app.js in the same directory:

// Initialize the map and set the view to New Delhi
const map = L.map('map').setView([28.6139, 77.2090], 12);

// Add OpenStreetMap tiles
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
  maxZoom: 19
}).addTo(map);

L.map('map') creates the map instance bound to your #map div. setView takes two arguments — a [latitude, longitude] array and a zoom level (1 = world view, 18-19 = street level). L.tileLayer loads the raster tiles that make up the visual background. The {s}, {z}, {x}, {y} placeholders are replaced dynamically by Leaflet as the user pans and zooms.

Open index.html in your browser and you should see a fully interactive map of New Delhi.


Step 3: Add a Basic Marker

Markers are the most common map element. Here is how to add one:

// Add a marker at India Gate
const marker = L.marker([28.6129, 77.2295]).addTo(map);

That is all it takes. Leaflet places a default blue pin at the coordinates you provide.


Step 4: Attach a Popup to the Marker

A marker without context is not particularly useful. Popups let you attach HTML content that appears when a user clicks the marker:

const marker = L.marker([28.6129, 77.2295])
  .addTo(map)
  .bindPopup('<b>India Gate</b><br>War memorial in the heart of New Delhi.')
  .openPopup();

bindPopup accepts a plain string or an HTML string. openPopup makes the popup visible on load — useful for drawing attention to a key location.


Step 5: Add Multiple Markers from Data

In real projects, you will rarely add markers one by one. More commonly, you will loop over an array of location data:

const locations = [
  {
    name: "India Gate",
    coords: [28.6129, 77.2295],
    description: "Iconic war memorial in central Delhi."
  },
  {
    name: "Qutub Minar",
    coords: [28.5244, 77.1855],
    description: "UNESCO World Heritage Site, a 73-metre minaret."
  },
  {
    name: "Humayun's Tomb",
    coords: [28.5933, 77.2507],
    description: "Mughal-era mausoleum, a precursor to the Taj Mahal."
  }
];

locations.forEach(location => {
  L.marker(location.coords)
    .addTo(map)
    .bindPopup(`<b>${location.name}</b><br>${location.description}`);
});

This pattern scales cleanly. You could replace the hardcoded array with data fetched from an API or loaded from a GeoJSON file.


Step 6: Draw Shapes on the Map

Beyond markers, Leaflet supports circles, polygons, polylines, and rectangles.

Circle:

L.circle([28.6139, 77.2090], {
  color: '#e63946',
  fillColor: '#e63946',
  fillOpacity: 0.3,
  radius: 1500  // in meters
}).addTo(map).bindPopup('1.5 km radius from city center');

Polygon:

L.polygon([
  [28.6300, 77.2200],
  [28.6400, 77.2350],
  [28.6250, 77.2500],
  [28.6150, 77.2350]
], {
  color: '#457b9d',
  fillOpacity: 0.4
}).addTo(map).bindPopup('A sample polygon area');

Step 7: Handle Map Events

Leaflet exposes a rich event system. A common use case is logging or reacting to where a user clicks:

map.on('click', function(e) {
  const { lat, lng } = e.latlng;
  L.popup()
    .setLatLng(e.latlng)
    .setContent(`You clicked at:<br><b>${lat.toFixed(5)}, ${lng.toFixed(5)}</b>`)
    .openOn(map);
});

This opens a popup at the clicked location showing its coordinates. This pattern is useful in field survey tools where users need to drop pins on demand.


Step 8: Load GeoJSON Data

GeoJSON is the standard format for geographic data on the web. Leaflet has native support for it:

const geojsonData = {
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [77.2090, 28.6139]  // GeoJSON uses [lng, lat]
      },
      "properties": {
        "name": "Connaught Place",
        "category": "Commercial Hub"
      }
    }
  ]
};

L.geoJSON(geojsonData, {
  onEachFeature: function(feature, layer) {
    if (feature.properties && feature.properties.name) {
      layer.bindPopup(
        `<b>${feature.properties.name}</b><br>${feature.properties.category}`
      );
    }
  }
}).addTo(map);

One important gotcha: GeoJSON coordinates are written as [longitude, latitude], which is the reverse of Leaflet’s [latitude, longitude] convention. This trips up almost everyone at some point.


Step 9: Use Custom Marker Icons

The default blue pin works, but custom icons make your map feel intentional and branded:

const customIcon = L.icon({
  iconUrl: 'https://cdn-icons-png.flaticon.com/512/684/684908.png',
  iconSize: [38, 38],       // pixel dimensions of the icon
  iconAnchor: [19, 38],     // the point of the icon that snaps to the coordinate
  popupAnchor: [0, -38]     // where the popup appears relative to the icon
});

L.marker([28.6129, 77.2295], { icon: customIcon })
  .addTo(map)
  .bindPopup('Custom icon marker');

iconAnchor and popupAnchor are the two values developers most often need to tweak. iconAnchor controls which pixel of the image touches the coordinate — for a pin, this should be the bottom-center tip.


Step 10: Add Layer Controls

For maps with multiple data themes, a layer toggle control lets users show and hide layers independently:

// Create separate layer groups
const landmarksLayer = L.layerGroup();
const servicesLayer = L.layerGroup();

L.marker([28.6129, 77.2295]).bindPopup('India Gate').addTo(landmarksLayer);
L.marker([28.6600, 77.2300]).bindPopup('A hospital').addTo(servicesLayer);

// Add both layers to the map
landmarksLayer.addTo(map);
servicesLayer.addTo(map);

// Add a layer control
L.control.layers(null, {
  'Landmarks': landmarksLayer,
  'Services': servicesLayer
}).addTo(map);

This renders a small toggle panel in the top-right corner of the map. Users can independently check or uncheck each overlay.


Putting It All Together

Here is the complete app.js combining everything covered above:

const map = L.map('map').setView([28.6139, 77.2090], 12);

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

const locations = [
  { name: "India Gate", coords: [28.6129, 77.2295], desc: "War memorial." },
  { name: "Qutub Minar", coords: [28.5244, 77.1855], desc: "UNESCO site." },
  { name: "Humayun's Tomb", coords: [28.5933, 77.2507], desc: "Mughal mausoleum." }
];

locations.forEach(loc => {
  L.marker(loc.coords).addTo(map).bindPopup(`<b>${loc.name}</b><br>${loc.desc}`);
});

L.circle([28.6139, 77.2090], {
  color: '#e63946',
  fillColor: '#e63946',
  fillOpacity: 0.2,
  radius: 2000
}).addTo(map).bindPopup('2 km radius from city center');

map.on('click', function(e) {
  const { lat, lng } = e.latlng;
  L.popup()
    .setLatLng(e.latlng)
    .setContent(`Clicked at: ${lat.toFixed(5)}, ${lng.toFixed(5)}`)
    .openOn(map);
});

Common Pitfalls to Avoid

Map renders blank. The most likely cause is that the #map container has no height. Leaflet cannot infer height from its parent — always set an explicit height in CSS.

GeoJSON coordinates appear in the wrong place. Remember that GeoJSON uses [lng, lat] while Leaflet uses [lat, lng]. These are easy to swap.

Leaflet CSS not loaded. If your markers look like broken images or popups have no styling, you have almost certainly forgotten to include the Leaflet stylesheet.

Tiles not loading. OpenStreetMap has usage policies. For production applications with significant traffic, use a hosted tile service like Mapbox, Stadia Maps, or Esri instead of the public OSM tile servers.


Where to Go from Here

Once you are comfortable with the basics, Leaflet’s plugin ecosystem opens up significantly more capability. Some directions worth exploring:

Leaflet.markercluster groups overlapping markers into clusters at lower zoom levels, which is essential for datasets with hundreds of points. Leaflet.heat renders heatmaps from point data. Leaflet-Geoman adds drawing and editing tools for polygons and lines directly on the map. For performance-intensive use cases with tens of thousands of features, pairing Leaflet with a vector tile source (via leaflet.vectorgrid) is worth investigating.

On the data side, learning to work with GeoJSON and then eventually Shapefiles through a conversion step will dramatically expand the kinds of maps you can build. Tools like QGIS and Mapshaper are useful companions to your Leaflet workflow.


Final Thoughts

Leaflet sits at a rare intersection: low barrier to entry, genuinely useful in production, and backed by a mature ecosystem. The ten steps in this article cover the building blocks that appear in most real-world mapping applications — markers, popups, shapes, events, GeoJSON, custom icons, and layer controls.

From here, the best way to deepen your skills is to pick a dataset that matters to you and build something with it. Public data sources like OpenStreetMap, government open data portals, and GADM administrative boundaries give you plenty of material to work with. The map you build around real data will teach you more than any tutorial can.


Built something with this guide? The Leaflet community forum and GitHub Discussions are good places to share work and get unstuck when you hit edge cases.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *