Skip to content

List and map together

View .md

The search-results pattern Airbnb, Zillow and most marketplaces use takes three wires:

User does Map does Code
Hovers a card in the list Lifts that card’s marker map.highlight(id) / map.highlight(null)
Clicks a marker Opens its card, and the list scrolls to the matching item select event
Pans or zooms The list shows what’s in view moveend event + map.bounds
<div class="results">
<ol id="list"></ol>
<instant-map id="map" center="-97.7431,30.2672" zoom="12"></instant-map>
</div>
<style>
.results { display: grid; grid-template-columns: 420px 1fr; height: 100dvh; }
#list { overflow: auto; margin: 0; padding: 16px; }
#list li[aria-current="true"] { outline: 2px solid #1c1f24; }
#map { height: 100%; }
@media (max-width: 800px) { .results { grid-template-columns: 1fr; grid-template-rows: 45dvh 1fr; } #map { order: -1; } }
</style>
<script type="module">
import "https://instantmap.org/instant-map.js";
const listings = await (await fetch("/api/listings")).json();
const map = document.getElementById("map");
const list = document.getElementById("list");
map.markers = listings.map((l) => ({ id: l.id, lng: l.lng, lat: l.lat, label: `$${l.price}`, title: l.title, image: l.photo, href: `/listings/${l.id}` }));
function render() {
const [west, south, east, north] = map.bounds;
const visible = listings.filter((l) => l.lng >= west && l.lng <= east && l.lat >= south && l.lat <= north);
list.replaceChildren(...visible.map((l) => {
const item = document.createElement("li");
item.id = `listing-${l.id}`;
item.innerHTML = `<a href="/listings/${l.id}">${l.title}, $${l.price}</a>`;
item.addEventListener("pointerenter", () => map.highlight(l.id));
item.addEventListener("pointerleave", () => map.highlight(null));
return item;
}));
}
map.addEventListener("moveend", render);
map.addEventListener("select", (event) => {
const id = event.detail.marker?.id;
for (const item of list.children) item.toggleAttribute("aria-current", item.id === `listing-${id}`);
document.getElementById(`listing-${id}`)?.scrollIntoView({ block: "nearest", behavior: "smooth" });
});
render();
</script>
  • Highlight, don’t select, on hover. highlight() only lifts the pin. select() opens a card, and opening cards as the pointer crosses a list is noisy.
  • A highlighted marker inside a cluster lifts the whole cluster, so the visitor still sees where it is.
  • Show all results after a search: map.fitBounds(markers, { padding: 64 }).
  • On phones, stack the map above the list at 40–50% of the viewport height. Keep the map’s scroll-zoom at cooperative so the page still scrolls past it.