Leaflet Store Locator with Search — Free HTML CSS JS Snippet

Leaflet Store Locator with Search · Misc · Plain HTML, CSS & JS · Live preview

What's included

Features

Real Haversine distance
Sorting uses actual great-circle distance, not an approximation.
Live re-sort on pan
"Nearest" updates to wherever the map is currently centered.
Two-way marker/list sync
Either one selects the same store through one function.
Search by name or address
Filtering pauses the distance re-sort while searching.
Single source of truth
One STORES array drives markers, list, and popups.
Free OpenStreetMap tiles
No API key or paid map provider required.

About this UI Snippet

Leaflet Store Locator with Search — a List and a Map That Never Disagree

Screenshot of the Leaflet Store Locator with Search snippet rendered live

A store locator is only as good as the connection between its list and its map — click a list item and the wrong pin highlights, or pan the map and the list stays frozen, and the whole thing reads as broken. This snippet keeps both in sync through one shared array of stores: clicking a marker's popup selects the matching list row, clicking a list row flies the map to that marker, and the list itself re-sorts by live distance from the map's current center on every pan.

Distance is computed with a real Haversine formula

Sorting "nearest first" only works if the distance is real. The distanceKm function implements the actual Haversine great-circle formula — converting each latitude/longitude pair to radians and computing the spherical distance — rather than a flat Pythagorean approximation, which becomes visibly wrong the farther apart two points are or the higher the latitude.

The list re-sorts on every map pan, not just on load

A moveend listener re-renders the list using the map's new center every time you pan or zoom, so "nearest" always means nearest to wherever you're currently looking — not nearest to some fixed original location. Searching pauses that behavior deliberately: while there's a search query, the list filters by name/address match instead of re-sorting on every pan, since a user actively searching for "Berkeley" doesn't want their results reshuffled by an incidental drag of the map underneath.

Selection state lives in one index, not scattered classes

Which item is active is tracked as a single activeIndex into the shared STORES array, re-applied as a CSS class on every render — so clicking a marker (which calls the exact same selectStore function a list click does) and clicking a list row produce identical, correct highlighting, with no separate code path to fall out of sync.

Reusing it

Swap the six sample stores for a real dataset (from a CMS, a database, or a JSON API), keep the same { name, addr, lat, lng } shape, and the search, sort, and sync logic all keep working unchanged. For dozens of locations rather than hundreds, this simple array-based approach is enough — past that, look at marker clustering, covered elsewhere in this collection.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to work out list-to-map synchronization from scratch. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how selectStore keeps a marker click and a list-item click producing identical results by routing both through one function and one shared index, and why the Haversine formula is necessary instead of a simpler flat-distance calculation. The same assistant can help optimize it — ask whether re-rendering the entire list on every single moveend event (which fires continuously during a drag) should be debounced for a larger dataset. It's also useful for extending the effect: ask it to add a "use my location" button using the Geolocation API to sort by real user proximity, a radius filter, or marker clustering for a dataset with hundreds of locations. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a store locator with a synced sidebar list and an interactive map using Leaflet.js (load Leaflet's CSS and JS from a CDN, no other library), in plain HTML, CSS, and JavaScript.

Requirements:
- Maintain a single array of store objects, each with a name, an address, and latitude/longitude coordinates, and render every store as both a map marker (with a popup showing its name and address) and a row in a sidebar list.
- Implement a real great-circle distance calculation (the Haversine formula, not a flat-plane approximation) between two latitude/longitude points, and use it to sort the sidebar list by distance from the map's current center, recalculating and re-rendering the sorted list every time the map is panned or zoomed.
- Add a text search input above the list that filters the list by store name or address as the user types; while a search query is active, the list should show filtered results instead of being re-sorted by the map's position.
- Make clicking a marker's popup and clicking its corresponding sidebar list item both trigger the exact same "select this store" behavior — highlighting that item in the list and, when triggered from the list, smoothly panning/flying the map to center on that marker and open its popup — routed through one shared function and one shared "currently selected" index so the two interactions can never disagree about which store is selected.
- Use free OpenStreetMap tile layers so the demo requires no API key.
- Show an empty-state message in the list when a search query matches no stores.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="sl-wrap">
  <div class="sl-panel">
    <div class="sl-head">
      <h1>Find a Store</h1>
      <input type="text" id="slSearch" class="sl-search" placeholder="Search by city or store name...">
    </div>
    <ul class="sl-list" id="slList"></ul>
  </div>
  <div class="sl-map" id="slMap"></div>
</div>

Step by step

How to Use

  1. 1
    Add the Leaflet CDNLoad leaflet.css and leaflet.js before the snippet's JS runs.
  2. 2
    Paste HTML, CSS, and JSSix sample stores render as markers with a synced list.
  3. 3
    Type in the search boxThe list filters by store name or address.
  4. 4
    Click a list itemThe map flies to that store and opens its popup.
  5. 5
    Click a map markerThe matching list row highlights.
  6. 6
    Pan the mapWith no search active, the list re-sorts by new distance.

Real-world uses

Common Use Cases

Retail and restaurant locators
The exact pattern for a "find a location near you" page.
Real estate listing maps
Same list-plus-map sync for property search results.
Service area finders
Repair shops, clinics, or dealership locator pages.
Event venue directories
Sortable-by-distance venue lists with map pins.
Franchise and multi-location sites
Pair with a dashboard admin panel for location management.
Learning Leaflet
A clear reference for markers, popups, and map events together.
Related: Leaflet Marker Clustering at Scale
See the Leaflet Marker Clustering snippet elsewhere in this collection for the pattern that replaces this one past a few dozen locations.

Got questions?

Frequently Asked Questions

A moveend listener re-renders the list sorted by distance from the map's new center every time you pan or zoom — that's what "nearest first" should mean. But while there's an active search query, the render function filters by text match instead, since re-sorting a user's search results out from under them because they nudged the map is more confusing than helpful.

It uses the real Haversine formula — converting latitude and longitude to radians and computing the great-circle distance across a sphere the size of Earth — rather than a flat-plane approximation. That matters because a naive Pythagorean distance on raw lat/lng degrees becomes increasingly wrong at higher latitudes and larger distances, where a degree of longitude covers much less real distance than a degree of latitude.

Both the marker's click handler and the list item's click handler call the same selectStore function, which sets activeIndex to that store's position in the shared STORES array and re-renders the list with that index's row marked active. Because there's only one function and one array, a marker click and a list click can never produce a different result for the same store.

Replace the STORES array with your own { name, addr, lat, lng } objects — everything downstream (markers, popups, search filtering, distance sorting) reads from that array's shape and needs no other changes. For a large dataset (hundreds of locations), pair this pattern with marker clustering instead of plotting every marker individually.

Initialize the Leaflet map once in a mount effect against a ref'd container, keep STORES and activeIndex in component state, and re-run marker creation only when the store data actually changes rather than on every render. Call map.remove() in your cleanup function to avoid leaking map instances on unmount.