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.

SKILL.md

≈57 tokens always: the name and description. ≈2.5k when used: this file. ≈8.6k more on demand in 8 files.

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 <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);
  // });
  //
  // For why addInteraction is preferred over map.on(), and how to pair it with
  // setFeatureState/appearances for hover and selection styling, see the
  // "interactions" reference file in the mapbox-style-patterns skill.

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

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 7 minutes 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.