Leaflet: an inspection viewer with GeoJSON
A complete example with fictional data, no keys and no basemap dependency.
Editorial review: 2026-09-23
Scope of the example
Leaflet supports a straightforward 2D viewer. Its setView method takes latitude, longitude; GeoJSON geometry takes longitude, latitude. This difference explains many misplaced maps. The popup uses textContent so an attribute is not interpreted as HTML.
Save the following as index.html in a lab directory and serve it with python3 -m http.server 8000 --bind 127.0.0.1. Open http://localhost:8000. Libraries download from a version-pinned CDN, so a connection is required. There are no basemap requests or real operational records.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Synthetic inspection</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<style>#map { height: 70vh } body { margin: 1rem; font: 16px sans-serif }</style>
<h1>Synthetic inspection</h1>
<p>Fictional record s1: pending inspection, latitude 6.24, longitude -75.58.</p>
<div id="map" aria-label="Fictional inspection location"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const map = L.map('map').setView([6.24, -75.58], 14)
const record = { type: 'Feature', properties: { id: 's1', status: 'pending' },
geometry: { type: 'Point', coordinates: [-75.58, 6.24] }
}
L.geoJSON(record, {
pointToLayer: (feature, latlng) => L.circleMarker(latlng, { radius: 10 }),
onEachFeature: (feature, layer) => {
const text = document.createElement('span')
text.textContent = feature.properties.id + ': ' + feature.properties.status
layer.bindPopup(text)
}
}).addTo(map)
</script>
</html>
Check the result
A point should appear near the center of an empty background. Test zooming, panning and resizing; check that the text remains accessible with a keyboard. The blank background is intentional: it separates data problems from a map provider's quotas, credentials and availability.
If the point does not appear, first inspect the console, script download and container height. Then check coordinate order and geometry type. When connecting an API, validate HTTP status and bound the volume before passing a response to the map. Do not download an entire operational database merely to hide attributes in the browser.
What an enterprise application still needs
Add authorized search, loading and error states, data and basemap attribution, and an equivalent table for tasks that do not require visual navigation. Establish a payload budget and test the team's slowest device. When adding a basemap, review its license, terms, quotas and provider charges: an open library does not include an unlimited free mapping service.
An application migration must also address printing, editing, permissions, exports and support. Use this lab to validate the visualization layer; assess those other capabilities separately before replacing an ArcGIS application.
Choose Leaflet for the task you are building
A small inspection viewer often needs straightforward markers, popups and raster basemaps. Start with the smallest rendering model that meets the task. If the application needs extensive vector-tile styling or large interactive feature sets, compare that requirement with MapLibre and measure a representative dataset. The choice is architectural, not a contest between logos.
Leaflet remains a client library. It does not provide record permissions, review workflow or authoritative edits. Keep read and write services explicit even if the first prototype loads a local GeoJSON file.
Render text safely in popups
Operational labels can contain characters interpreted as HTML. Construct text nodes rather than interpolating untrusted attributes into markup. For a GeoJSON layer, the popup setup can use:
const inspectionLayer = L.geoJSON(featureCollection, {
onEachFeature: (feature, layer) => {
const container = document.createElement('div')
const title = document.createElement('strong')
title.textContent = String(feature.properties?.name ?? 'Unnamed asset')
container.append(title)
layer.bindPopup(container)
}
})
inspectionLayer.addTo(map)
This fragment assumes the complete page has already created map and loaded an authorized featureCollection. It demonstrates safe text handling, not authentication. For attachment links, validate permitted URL schemes and destinations and avoid exposing private storage URLs to unauthorized users.
Keep a record list synchronized with the map
Use the same stable business key in the map feature, list row and detail endpoint. Clicking a row should select the intended marker and reveal its details; changing the filter should update both views. Keep the selected record visible or clearly announce when it is outside the filter.
Provide counts for matching and displayed records, especially if the API paginates. A map containing 100 points from a 5,000-record result must not be labeled as the complete inventory. Avoid rendering thousands of DOM markers without measuring their effect; use a bounded query or an appropriate renderer and clustering approach for the actual interaction.
Test coordinate order and content explicitly
GeoJSON stores longitude before latitude. Leaflet APIs that accept LatLng commonly use latitude before longitude, while its GeoJSON loader handles the GeoJSON convention. Keep these boundaries explicit when adding custom markers or fitting bounds. Test one known synthetic coordinate and inspect the result rather than “fixing” every input by swapping it globally.
Include a null optional field, an accented name, a long name and a record without geometry. The table should remain usable when a record cannot be mapped. Give missing locations a visible status and a review route instead of silently dropping them from the operational count.
Replace a viewer without dropping workflow requirements
Record whether the existing tool supports measurement, printing, exports, authenticated attachments, editing or mobile inspection. Implement and validate those tasks individually. A screenshot comparison does not test an export's CRS or whether a private attachment leaks through a link.
Use a test account to attempt access to a record outside its scope. Verify the server rejects it even when the identifier is known. For a purely public dataset, check attribution and permitted reuse before making downloads convenient. Finish with keyboard navigation, narrow-screen layout and a failed-network state. These checks turn a marker demo into an assessable application.
Reference: Leaflet GeoJSON and popup APIs.
Sources and documentation
Next step
Continue in the Open GIS collection. For a specific project, use the total-cost calculator and request an assessment.