Leaflet Location Picker (Click to Set Coordinates) — Free Snippet
Leaflet Location Picker (Click to Set Coordinates) · Forms · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="lp-wrap">
<div class="lp-form">
<h1>Set Pickup Location</h1>
<label class="lp-field">Latitude
<input type="text" id="lpLat" placeholder="Click the map">
</label>
<label class="lp-field">Longitude
<input type="text" id="lpLng" placeholder="Click the map">
</label>
<label class="lp-field">Address (search)
<input type="text" id="lpSearch" placeholder="Type an address or place...">
<div class="lp-suggestions" id="lpSuggestions"></div>
</label>
<p class="lp-hint" id="lpHint">Click anywhere on the map, drag the pin, or search an address above.</p>
<button class="lp-confirm" id="lpConfirm" type="button" disabled>Confirm Location</button>
</div>
<div class="lp-map" id="lpMap"></div>
</div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif}
.lp-wrap{display:flex;height:100vh;min-height:480px}
.lp-form{width:280px;flex-shrink:0;background:#fff;border-right:1px solid #e2e8f0;padding:18px;display:flex;flex-direction:column;gap:12px;position:relative;z-index:2}
.lp-form h1{font-size:15px;font-weight:800;color:#0f172a}
.lp-field{display:flex;flex-direction:column;gap:5px;font-size:11.5px;font-weight:700;color:#64748b;position:relative}
.lp-field input{padding:8px 10px;border:1.5px solid #e2e8f0;border-radius:8px;font-size:13px;color:#0f172a;font-weight:600;outline:none}
.lp-field input:focus{border-color:#6366f1}
.lp-suggestions{position:absolute;top:calc(100% + 2px);left:0;right:0;background:#fff;border:1px solid #e2e8f0;border-radius:8px;box-shadow:0 8px 20px rgba(0,0,0,.1);overflow:hidden;display:none;z-index:5}
.lp-suggestion{padding:8px 10px;font-size:12px;font-weight:600;color:#334155;cursor:pointer}
.lp-suggestion:hover{background:#f1f5f9}
.lp-hint{font-size:11.5px;color:#94a3b8;line-height:1.5}
.lp-confirm{margin-top:auto;padding:11px;border:none;border-radius:9px;background:#6366f1;color:#fff;font:800 13px system-ui;cursor:pointer}
.lp-confirm:disabled{background:#c7d2fe;cursor:default}
.lp-confirm:not(:disabled):hover{background:#5457e5}
.lp-map{flex:1}// A small fixed "gazetteer" standing in for a real geocoding API -- enough
// to demonstrate search-to-pin without a network dependency in the sandbox.
var PLACES = [
{ label: 'Golden Gate Park, San Francisco', lat: 37.7694, lng: -122.4862 },
{ label: 'Ferry Building, San Francisco', lat: 37.7955, lng: -122.3937 },
{ label: 'Fisherman\u2019s Wharf, San Francisco', lat: 37.8080, lng: -122.4177 },
{ label: 'Mission Dolores Park, San Francisco', lat: 37.7596, lng: -122.4269 },
{ label: 'Oracle Park, San Francisco', lat: 37.7786, lng: -122.3893 },
];
var map = L.map('lpMap').setView([37.775, -122.42], 12);
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri',
maxZoom: 19,
}).addTo(map);
var latInput = document.getElementById('lpLat');
var lngInput = document.getElementById('lpLng');
var searchInput = document.getElementById('lpSearch');
var suggestionsEl = document.getElementById('lpSuggestions');
var confirmBtn = document.getElementById('lpConfirm');
var hintEl = document.getElementById('lpHint');
var marker = null;
function setLocation(lat, lng, pan) {
latInput.value = lat.toFixed(5);
lngInput.value = lng.toFixed(5);
confirmBtn.disabled = false;
if (!marker) {
marker = L.marker([lat, lng], { draggable: true }).addTo(map);
marker.on('drag', function (e) {
var p = e.target.getLatLng();
latInput.value = p.lat.toFixed(5);
lngInput.value = p.lng.toFixed(5);
});
} else {
marker.setLatLng([lat, lng]);
}
if (pan) map.flyTo([lat, lng], 15, { duration: 0.5 });
}
map.on('click', function (e) { setLocation(e.latlng.lat, e.latlng.lng, false); });
// Typed coordinates should move the pin too -- the form is bidirectional,
// not just a read-only display of what was clicked.
function tryManualEntry() {
var lat = parseFloat(latInput.value), lng = parseFloat(lngInput.value);
if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
setLocation(lat, lng, true);
}
}
latInput.addEventListener('change', tryManualEntry);
lngInput.addEventListener('change', tryManualEntry);
searchInput.addEventListener('input', function () {
var q = searchInput.value.trim().toLowerCase();
if (!q) { suggestionsEl.style.display = 'none'; return; }
var matches = PLACES.filter(function (p) { return p.label.toLowerCase().indexOf(q) !== -1; });
if (!matches.length) { suggestionsEl.style.display = 'none'; return; }
suggestionsEl.innerHTML = '';
matches.forEach(function (p) {
var el = document.createElement('div');
el.className = 'lp-suggestion';
el.textContent = p.label;
el.addEventListener('click', function () {
searchInput.value = p.label;
suggestionsEl.style.display = 'none';
setLocation(p.lat, p.lng, true);
});
suggestionsEl.appendChild(el);
});
suggestionsEl.style.display = 'block';
});
document.addEventListener('click', function (e) {
if (!e.target.closest('.lp-field')) suggestionsEl.style.display = 'none';
});
confirmBtn.addEventListener('click', function () {
hintEl.textContent = 'Confirmed: ' + latInput.value + ', ' + lngInput.value;
confirmBtn.textContent = 'Confirmed \u2713';
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Leaflet Location Picker — Four Inputs, One Coordinate, No Way to Disagree

A location picker that only supports clicking the map is missing half its users — some already know the exact coordinates, some know an address, and some just want to drag a pin to fine-tune a rough guess. This snippet supports all four paths (map click, pin drag, typed coordinates, and place search) by routing every one of them through a single setLocation(lat, lng, pan) function, so no path can produce a state the others don't recognize.
One function, four callers
Clicking the map, finishing a drag, selecting a search suggestion, and typing valid coordinates all eventually call setLocation — it's the only place that updates the latitude/longitude inputs, moves or creates the marker, and enables the confirm button. That's what guarantees the form fields and the map pin can never show two different locations at once, regardless of which interaction the user chose.
Manual coordinate entry is validated before it moves anything
Typing into the latitude or longitude field doesn't move the pin on every keystroke — it validates on change (when the field loses focus or Enter is pressed), checking both that the values parse as numbers and that they fall within real coordinate ranges (-90 to 90, -180 to 180) before calling setLocation. An invalid or incomplete entry simply doesn't move anything, rather than crashing or panning to NaN, NaN.
The place search is a local filter, not a live geocoding call
To keep the demo self-contained and free of network dependencies inside the sandboxed preview, search matches against a small fixed list of named places rather than a real geocoding API — the interaction pattern (type, see suggestions, click one, pin moves and map flies there) is exactly what a real fetch-based geocoder integration would look like, with the local array standing in for the API response.
Suggestions close on outside click, not just on selection
A document-level click listener closes the suggestions dropdown whenever a click lands outside the search field's container — the kind of small interaction detail (matching what every real autocomplete does) that's easy to skip and immediately feels unfinished without it.
Reusing it
Swap the local PLACES array for a real geocoding API call (Nominatim, Mapbox, Google) inside the same input handler, keep the rest of the four-way sync exactly as is, and this becomes a production-ready address picker for checkout flows, pickup/dropoff selection, or any form that needs a precise geographic point.
Step by step
How to Use
- 1Add the Leaflet CDNLoad leaflet.css and leaflet.js before the snippet's JS runs.
- 2Paste HTML, CSS, and JSA form and map render side by side, both empty.
- 3Click anywhere on the mapA pin drops and the coordinate fields fill in.
- 4Drag the pinThe coordinate fields update live as you move it.
- 5Type coordinates directlyTab or click away to move the pin to that exact point.
- 6Search a place namePick a suggestion to fly the map there and drop the pin.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Every interaction path — clicking the map, dragging the pin, selecting a search result, or typing valid coordinates — ultimately calls the same setLocation function, which is the only code that updates the latitude/longitude input values, moves or creates the marker, and enables the confirm button. Because there is exactly one function responsible for that state, no interaction path can leave the form and the map pin showing different locations.
The manual-entry handler runs on the field's change event and checks that both values parse as real numbers and fall within valid coordinate ranges (latitude between -90 and 90, longitude between -180 and 180) before calling setLocation. If the check fails — an empty field, non-numeric text, or an out-of-range value — nothing happens: the pin stays where it was rather than jumping to an invalid or NaN position.
No — to keep this snippet self-contained and free of network calls inside a sandboxed preview, it searches against a small fixed local array of named places. The interaction it demonstrates (type, see filtered suggestions, click one, the map flies there and the pin drops) is exactly the pattern you would wire up to a real geocoding API's search endpoint, with the local array simply standing in for that API's response.
The marker's drag event (which fires continuously throughout the gesture) reads the marker's current position on every frame of the drag and writes it straight into the latitude and longitude fields. Using dragend instead would leave the form fields stale and out of sync with the pin's actual position for the entire duration of the drag, which reads as a bug even if it self-corrects at the end.
Replace the local PLACES.filter() logic inside the search input's handler with a fetch call to a real geocoding API (Nominatim, Mapbox Geocoding, Google Places), map its response results into the same { label, lat, lng } shape the suggestions list already expects, and everything downstream — rendering suggestions, selecting one, flying the map, dropping the pin — needs no other changes.