All skills
mapbox avatar

/mapbox-store-locator-patterns

@9077a82 official
by mapboxmapbox/mapbox-agent-skills80 stars
17

Common patterns for building store locators, restaurant finders, and location-based search applications with Mapbox. Covers marker display, filtering, distance calculation, and interactive lists.

Use this Skill: https://skilld.dev/gh/mapbox/mapbox-agent-skills/mapbox-store-locator-patterns

This session only. Nothing lands on disk.

AGENTS.md

≈2.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Store Locator Patterns

Quick reference for building store locators and location finders with Mapbox.

Architecture

Core Components:

  1. Map with markers
  2. Location data (GeoJSON)
  3. Interactive list
  4. Search/filter
  5. User location + distance
  6. Directions (optional)

Data Structure

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [-77.034084, 38.909671]
      },
      "properties": {
        "id": "store-001",
        "name": "Downtown Store",
        "address": "123 Main St, DC 20001",
        "phone": "(202) 555-0123",
        "category": "retail",
        "hours": "Mon-Sat: 9am-9pm"
      }
    }
  ]
}

Marker Strategies

Locations Strategy Implementation
< 100 HTML Markers new mapboxgl.Marker()
100-1000 Symbol Layer addLayer({ type: 'symbol' })
> 1000 Clustering cluster: true in source

HTML Markers Pattern

stores.features.forEach((store) => {
  const marker = new mapboxgl.Marker()
    .setLngLat(store.geometry.coordinates)
    .setPopup(
      new mapboxgl.Popup().setHTML(`
        <h3>${store.properties.name}</h3>
        <p>${store.properties.address}</p>
      `)
    )
    .addTo(map);
});

Symbol Layer Pattern

map.on('load', () => {
  map.addSource('stores', {
    type: 'geojson',
    data: stores
  });

  map.addLayer({
    id: 'stores',
    type: 'symbol',
    source: 'stores',
    layout: {
      'icon-image': 'custom-marker',
      'icon-size': 0.8,
      'text-field': ['get', 'name'],
      'text-offset': [0, 1.5]
    }
  });

  // Using Interactions API (recommended)
  map.addInteraction('store-click', {
    type: 'click',
    target: { layerId: 'stores' },
    handler: (e) => {
      const store = e.feature;
      showStoreDetails(store);
    }
  });

  // Or using traditional event listener
  // map.on('click', 'stores', (e) => {
  //   const store = e.features[0];
  //   showStoreDetails(store);
  // });
});

For why addInteraction is preferred over map.on(), and how to pair it with setFeatureState/appearances for hover and selection styling, see mapbox-style-patterns/references/interactions.md.

Clustering Pattern

map.addSource('stores', {
  type: 'geojson',
  data: stores,
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50
});

// Clusters
map.addLayer({
  id: 'clusters',
  type: 'circle',
  source: 'stores',
  filter: ['has', 'point_count'],
  paint: {
    'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 10, '#f1f075', 30, '#f28cb1'],
    'circle-radius': ['step', ['get', 'point_count'], 20, 10, 30, 30, 40]
  }
});

// Unclustered points
map.addLayer({
  id: 'unclustered-point',
  type: 'circle',
  source: 'stores',
  filter: ['!', ['has', 'point_count']],
  paint: { 'circle-color': '#11b4da', 'circle-radius': 8 }
});

Interactive List

function buildLocationList(stores) {
  const container = document.getElementById('listings');
  container.innerHTML = '';

  stores.features.forEach((store) => {
    const listing = document.createElement('div');
    listing.className = 'listing';
    listing.innerHTML = `
      <a href="#" class="title">${store.properties.name}</a>
      <p>${store.properties.address}</p>
      ${store.properties.distance ? `<p class="distance">${store.properties.distance} mi</p>` : ''}
    `;

    listing.querySelector('.title').addEventListener('click', (e) => {
      e.preventDefault();
      flyToStore(store);
      createPopup(store);
    });

    container.appendChild(listing);
  });
}

function flyToStore(store) {
  map.flyTo({
    center: store.geometry.coordinates,
    zoom: 15
  });
}

Search/Filter

Text Search:

function filterStores(query) {
  const filtered = {
    type: 'FeatureCollection',
    features: stores.features.filter((store) => {
      const name = store.properties.name.toLowerCase();
      const address = store.properties.address.toLowerCase();
      return name.includes(query.toLowerCase()) || address.includes(query.toLowerCase());
    })
  };

  map.getSource('stores').setData(filtered);
  buildLocationList(filtered);
}

document.getElementById('search').addEventListener('input', (e) => {
  filterStores(e.target.value);
});

Category Filter:

function filterByCategory(category) {
  const filtered =
    category === 'all'
      ? stores
      : {
          type: 'FeatureCollection',
          features: stores.features.filter((s) => s.properties.category === category)
        };

  map.getSource('stores').setData(filtered);
  buildLocationList(filtered);
}

Distance Calculation

Using Turf.js (recommended):

import * as turf from '@turf/turf';

// Calculate distance between two points
function calculateDistance(from, to) {
  const fromPoint = turf.point(from);
  const toPoint = turf.point(to);
  const distance = turf.distance(fromPoint, toPoint, { units: 'miles' });
  return distance.toFixed(1);
}

// Get user location
navigator.geolocation.getCurrentPosition((position) => {
  const userLocation = [position.coords.longitude, position.coords.latitude];

  // Add distances
  stores.features = stores.features.map((store) => ({
    ...store,
    properties: {
      ...store.properties,
      distance: calculateDistance(userLocation, store.geometry.coordinates)
    }
  }));

  // Sort by distance
  stores.features.sort((a, b) => a.properties.distance - b.properties.distance);

  buildLocationList(stores);
});

Directions Integration

async function getDirections(from, to) {
  const query = await fetch(
    `https://api.mapbox.com/directions/v5/mapbox/driving/` +
      `${from[0]},${from[1]};${to[0]},${to[1]}?` +
      `geometries=geojson&access_token=${mapboxgl.accessToken}`
  );

  const route = (await query.json()).routes[0];

  // Display route
  map.getSource('route').setData({
    type: 'Feature',
    geometry: route.geometry
  });

  // Add route layer if not exists
  if (!map.getLayer('route')) {
    map.addLayer({
      id: 'route',
      type: 'line',
      source: 'route',
      paint: {
        'line-color': '#3b9ddd',
        'line-width': 5
      }
    });
  }

  return {
    duration: Math.floor(route.duration / 60),
    distance: (route.distance * 0.000621371).toFixed(1)
  };
}

Layout Patterns

Sidebar + Map:

<div style="display: flex; height: 100vh;">
  <div class="sidebar" style="width: 400px; overflow-y: scroll;">
    <input type="text" id="search" placeholder="Search..." />
    <div id="listings"></div>
  </div>
  <div id="map" style="flex: 1;"></div>
</div>

Mobile Responsive:

@media (max-width: 768px) {
  #app {
    flex-direction: column;
  }
  .sidebar {
    width: 100%;
    height: 40vh;
  }
  #map {
    height: 60vh;
  }
}

Performance Tips

// Debounce search
function debounce(func, wait) {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), wait);
  };
}

const debouncedSearch = debounce(filterStores, 300);

Geolocation Control

map.addControl(
  new mapboxgl.GeolocateControl({
    positionOptions: { enableHighAccuracy: true },
    trackUserLocation: true,
    showUserHeading: true
  })
);

Common Patterns

Restaurant Finder:

  • Category filters (cuisine type)
  • Price range filters
  • Rating display
  • Hours of operation
  • Delivery/pickup options

Office Locator:

  • Department filters
  • Floor/building numbers
  • Contact information
  • Meeting room availability

Retail Store Finder:

  • Inventory availability
  • Store hours
  • Services offered
  • Appointment booking

Quick Decisions

Need clustering? → Yes if > 1000 locations

Need search? → Always include for > 10 locations

Need directions? → Yes for physical locations users visit

Need distance sorting? → Yes if user location available

Need filters? → Yes if > 20 locations or multiple categories

Resources

Source: SKILL.md on GitHub

1 warning14d5 checks · Risk SAFE
  • Gen Agent Trust Hub14d

    The skill provides patterns for building map-based applications using Mapbox. It includes minor security risks related to how external location data is rendered in the user interface, which could allow for script execution if the data source is untrusted.

  • Socket14d

    No alerts

  • Snyk14d

    Risk: LOW · No issues

  • Runlayer7mo

    2/2 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 9077a82. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 4 hours ago.

Activeupdated 2 weeks ago
  • mapbox
  • store-locator
  • geojson
  • markers
  • filtering
  • geolocation
  • turf
  • javascript
  • interactive-map

README badge

README badge for mapbox/mapbox-agent-skills/mapbox-store-locator-patterns

Provides patterns for building store locators, restaurant finders, and location-based search apps with Mapbox GL JS, covering marker display (symbol layers for 100–1000 locations, HTML markers for fewer), filtering, distance calculation, and interactive location lists. Includes data structure templates, popup and directions integration, and references for geolocation, search, clustering, and React implementations.

Generated from the current SKILL.md.

Does this skill work with HTML markers or only symbol layers?
The main implementation uses symbol layers for 100–1,000 locations. HTML markers and clustering patterns are covered in the `references/markers.md` reference file for smaller or larger datasets.
What libraries are required besides Mapbox GL JS?
Turf.js (@turf/turf) is required for spatial calculations like distance. It's listed as a dependency and installed via npm.
Does this skill handle user geolocation and directions?
Yes. The skill references geolocation and directions patterns in `references/geolocation-directions.md`, including distance calculation and route integration.
Is there a React implementation included?
React patterns and variations are documented in `references/variations-react.md`, along with mobile-first and fullscreen layouts.
What's the expected performance threshold for marker count?
Symbol layers scale to 100–1,000 locations efficiently on the GPU. Below 100, HTML markers work fine; above 1,000, clustering is recommended.

Generated from the current SKILL.md. These answers refresh after source changes.