🎁 Get the FREE AI Skills Starter Guide β€” Subscribe β†’
BytesAgainBytesAgain
πŸ¦€ ClawHub

Mapbox Store Locator Patterns

by @mapbox

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

Versionv1.0.0
Downloads529
TERMINAL
clawhub install mapbox-store-locator-patterns

πŸ“– About This Skill


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

Store Locator Patterns Skill

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

When to Use This Skill

Use this skill when building applications that:

  • Display multiple locations on a map (stores, restaurants, offices, etc.)
  • Allow users to filter or search locations
  • Calculate distances from user location
  • Provide interactive lists synced with map markers
  • Show location details in popups or side panels
  • Integrate directions to selected locations
  • Dependencies

    Required:

  • Mapbox GL JS v3.x
  • @turf/turf - For spatial calculations (distance, area, etc.)
  • Installation:

    npm install mapbox-gl @turf/turf
    

    Core Architecture

    Pattern Overview

    A typical store locator consists of:

    1. Map Display - Shows all locations as markers 2. Location Data - GeoJSON with store/location information 3. Interactive List - Side panel listing all locations 4. Filtering - Search, category filters, distance filters 5. Detail View - Popup or panel with location details 6. User Location - Geolocation for distance calculation. For the blue dot location indicator, use the built-in mapboxgl.GeolocateControl β€” simpler than custom markers. 7. Directions - Route to selected location (optional)

    Data Structure

    GeoJSON format for locations:

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

    Key properties:

  • id - Unique identifier for each location
  • name - Display name
  • address - Full address for display and geocoding
  • coordinates - [longitude, latitude] format
  • category - For filtering (retail, restaurant, office, etc.)
  • Custom properties as needed (hours, phone, website, etc.)
  • Basic Store Locator Implementation

    Step 1: Initialize Map and Data

    import mapboxgl from 'mapbox-gl';
    import 'mapbox-gl/dist/mapbox-gl.css';

    mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';

    // Store locations data const stores = { type: 'FeatureCollection', features: [ { type: 'Feature', geometry: { type: 'Point', coordinates: [-77.034084, 38.909671] }, properties: { id: 'store-001', name: 'Downtown Store', address: '123 Main St, Washington, DC 20001', phone: '(202) 555-0123', category: 'retail' } } // ... more stores ] };

    const map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/standard', center: [-77.034084, 38.909671], zoom: 11 });

    Step 2: Add Markers to Map

    Marker strategy by location count:

    | Count | Strategy | Reason | | ------------------- | -------------------------- | ------------------------------------------------------------------------------ | | Fewer than 100 | HTML Markers | Full DOM/CSS control; DOM node count is manageable | | 100–1,000 | Symbol Layer (default) | Renders on the GPU via WebGL β€” one , zero per-point DOM elements | | More than 1,000 | Clustering | Reduces visual clutter at large scale |

    > HTML Markers create one DOM element per point. Beyond ~100 locations the browser spends too much time on layout/paint. Symbol layers bypass the DOM entirely β€” the GPU draws all points in a single WebGL draw call.

    Symbol Layer implementation (best for 100–1,000 locations). For HTML Markers (fewer than 100) or Clustering (more than 1,000), see references/markers.md.

    map.on('load', () => {
      // Add store data as source
      map.addSource('stores', {
        type: 'geojson',
        data: stores
      });

    // Add custom marker image map.loadImage('/marker-icon.png', (error, image) => { if (error) throw error; map.addImage('custom-marker', image);

    // Add symbol layer map.addLayer({ id: 'stores-layer', type: 'symbol', source: 'stores', layout: { 'icon-image': 'custom-marker', 'icon-size': 0.8, 'icon-allow-overlap': true, 'text-field': ['get', 'name'], 'text-font': ['Open Sans Bold', 'Arial Unicode MS Bold'], 'text-offset': [0, 1.5], 'text-anchor': 'top', 'text-size': 12 } }); });

    // Handle marker clicks using Interactions API (recommended) map.addInteraction('store-click', { type: 'click', target: { layerId: 'stores-layer' }, handler: (e) => { const store = e.feature; flyToStore(store); createPopup(store); } });

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

    // Change cursor on hover map.on('mouseenter', 'stores-layer', () => { map.getCanvas().style.cursor = 'pointer'; });

    map.on('mouseleave', 'stores-layer', () => { map.getCanvas().style.cursor = ''; }); });

    Step 3: Build Interactive Location List

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

    stores.features.forEach((store, index) => { const listing = listingContainer.appendChild(document.createElement('div')); listing.id = listing-${store.properties.id}; listing.className = 'listing';

    const link = listing.appendChild(document.createElement('a')); link.href = '#'; link.className = 'title'; link.id = link-${store.properties.id}; link.innerHTML = store.properties.name;

    const details = listing.appendChild(document.createElement('div')); details.innerHTML =

    ${store.properties.address}

    ${store.properties.phone || ''}

    ;

    // Handle listing click link.addEventListener('click', (e) => { e.preventDefault(); flyToStore(store); createPopup(store); highlightListing(store.properties.id); }); }); }

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

    function createPopup(store) { const popups = document.getElementsByClassName('mapboxgl-popup'); // Remove existing popups if (popups[0]) popups[0].remove();

    new mapboxgl.Popup({ closeOnClick: true }) .setLngLat(store.geometry.coordinates) .setHTML(

    ${store.properties.name}

    ${store.properties.address}

    ${store.properties.phone}

    ${store.properties.website ?
    Visit Website : ''} ) .addTo(map); }

    // IMPORTANT: highlightListing MUST include scrollIntoView β€” without it, // selecting a marker on the map won't scroll the sidebar to the listing. function highlightListing(id) { // Remove existing highlights const activeItem = document.getElementsByClassName('active'); if (activeItem[0]) { activeItem[0].classList.remove('active'); }

    // Add highlight to selected listing const listing = document.getElementById(listing-${id}); listing.classList.add('active');

    // Scroll the selected listing into view (critical UX requirement) listing.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }

    // Build the list on load map.on('load', () => { buildLocationList(stores); });

    Reference Files

    Load these references for additional patterns as needed:

    | Reference | File | Contents | | ------------------------- | -------------------------------------- | ---------------------------------------------------------------- | | HTML Markers & Clustering | references/markers.md | HTML Markers (< 100 locations), Clustering (> 1000 locations) | | Search & Filter | references/search-filter.md | Text search, category filter | | Geolocation & Directions | references/geolocation-directions.md | User location, distance calculation, route directions | | Styling & Layout | references/styling-layout.md | Full HTML/CSS layout, custom marker CSS | | Performance & A11y | references/optimization-a11y.md | Debounced search, data management, error handling, accessibility | | Variations & React | references/variations-react.md | Mobile-first, fullscreen, map-only, React implementation |

    Resources

  • Turf.js - Spatial analysis library (recommended for distance calculations)
  • Mapbox GL JS API
  • Interactions API Guide
  • GeoJSON Specification
  • Directions API
  • Store Locator Tutorial