mapbox-store-locator-patterns

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

INSTALLATION
npx skills add https://github.com/mapbox/mapbox-agent-skills --skill mapbox-store-locator-patterns
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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:

  • Map Display - Shows all locations as markers
  • Location Data - GeoJSON with store/location information
  • Interactive List - Side panel listing all locations
  • Filtering - Search, category filters, distance filters
  • Detail View - Popup or panel with location details
  • User Location - Geolocation for distance calculation. For the blue dot location indicator, use the built-in mapboxgl.GeolocateControl — simpler than custom markers.
  • 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 <canvas>, 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 = `

      <p>${store.properties.address}</p>

      <p>${store.properties.phone || ''}</p>

    `;

    // 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(

      `<h3>${store.properties.name}</h3>

       <p>${store.properties.address}</p>

       <p>${store.properties.phone}</p>

       ${store.properties.website ? `<a href="${store.properties.website}" target="_blank">Visit Website</a>` : ''}`

    )

    .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 &#x26; Clustering

references/markers.md

HTML Markers (< 100 locations), Clustering (> 1000 locations)

Search &#x26; Filter

references/search-filter.md

Text search, category filter

Geolocation &#x26; Directions

references/geolocation-directions.md

User location, distance calculation, route directions

Styling &#x26; Layout

references/styling-layout.md

Full HTML/CSS layout, custom marker CSS

Performance &#x26; A11y

references/optimization-a11y.md

Debounced search, data management, error handling, accessibility

Variations &#x26; React

references/variations-react.md

Mobile-first, fullscreen, map-only, React implementation

Resources

  • Turf.js - Spatial analysis library (recommended for distance calculations)
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card