Back to Developer Home

Storepoint Locator Widget JavaScript API

Customize and extend your embedded store locator with our Locator Widget JavaScript API. Listen for events, trigger actions, and create seamless integrations between your application and the Storepoint locator widget.

Get Started →

Installation

Get Your Embed Code

Find your unique embed code (and public widget ID) on the 'Embed Locator' page in your dashboard.

Get Embed Code

Copy the embed code from your dashboard and paste it into your website:

HTML
<!-- Storepoint Widget Embed Start -->
<div id="storepoint-widget"></div>
<script>
window.Storepoint=window.Storepoint||{_q:[],on:function(e,c){this._q.push([e,c])}};
window.loadStorepoint=function(){new StorepointWidget('YOUR_WIDGET_ID','#storepoint-widget',{});};
!function(){var t=document.createElement("script");t.type="text/javascript";t.async=!0;t.src="https://widget.storepoint.co/embed.js";t.onload=function(){"function"==typeof window.loadStorepoint&&window.loadStorepoint()};document.head.appendChild(t);}();
</script>
<!-- Storepoint Widget Embed End -->

Replace YOUR_WIDGET_ID with your public widget ID, or use the embed code directly from your dashboard embed page. To try the widget without an account, use 'local' as the widget ID. See Local Testing Mode.

Timing-Safe: The first line creates a global Storepoint object. You can call Storepoint.on() anywhere on your page - before or after the widget loads - and it will work correctly.

Configuration Options

Pass configuration options as the third parameter when initializing the widget to customize behavior and override dashboard settings.

initialState

Set the initial search state of the widget. Useful for programmatically pre-loading a search, pre-filling the search bar, or automatically detecting the user's location.

JavaScript
var storepoint = new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', {
    initialState: {
        search_text: 'New York, NY',    // Pre-fill search bar
        lat: 40.7128,                   // Latitude to search from
        lng: -74.0060,                  // Longitude to search from
        radius: 50,                     // Search radius (uses widget's distance unit)
        auto_detect_location: true      // Automatically detect user's location via IP
    }
});

Properties

Property Type Description
search_text string Text to display in the search bar. Does not trigger a search by itself unless combined with lat/lng.
lat number Latitude to search from. Must be between -90 and 90. When combined with lng, triggers a search.
lng number Longitude to search from. Must be between -180 and 180. When combined with lat, triggers a search.
radius number Search radius. Must be a positive number. Uses the distance unit configured in your dashboard (miles or kilometers).
auto_detect_location boolean When true, automatically detects the user's location using IP geolocation and performs a search. Overrides lat/lng if both are provided.

Examples

Pre-fill search with coordinates

JavaScript
var storepoint = new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', {
    initialState: {
        search_text: 'Los Angeles',
        lat: 34.0522,
        lng: -118.2437,
        radius: 25
    }
});

Auto-detect user location

JavaScript
var storepoint = new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', {
    initialState: {
        auto_detect_location: true,
        radius: 10
    }
});

URL parameters (e.g., ?search=Chicago) take precedence over initialState and will override these settings.

defaultView

Set defaultView to 'none' to show only the search bar and filters when the widget first loads. The results list appears after a visitor searches or filters. This overrides the default view in your dashboard for this specific embed only.

JavaScript
var storepoint = new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', {
    defaultView: 'none'
});

Example: Search-only locator (no map)

Combine with mapOptions.disabled to create a search-first experience with a full-width results list.

JavaScript
var storepoint = new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', {
    mapOptions: { disabled: true },
    defaultView: 'none'
});

Your dashboard preview and any other embeds without this option continue to use the default view from your dashboard settings.

mapOptions

Map configuration options. These settings allow you to override dashboard map settings or access deeper Mapbox GL JS configuration options not exposed in the dashboard.

mapOptions.disabled

Hide the map entirely for this embed. The widget displays the search bar and results list only. Your dashboard preview continues to show the map.

JavaScript
var storepoint = new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', {
    mapOptions: {
        disabled: true
    }
});

When the map is disabled, you may want to add CSS to make the results list span the full width: #storepoint-widget .storepoint-container .storepoint-results { grid-column: 1 / -1; }

mapOptions.initialFocus

Set the initial map viewport before any search is performed. Useful for centering the map on a specific region, city, or country.

JavaScript
var storepoint = new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', {
    mapOptions: {
        initialFocus: {
            latitude: 51.5074,     // Center latitude
            longitude: -0.1278,    // Center longitude
            zoom: 10               // Zoom level (0-22)
        }
    }
});

Properties

Property Type Range Description
latitude number -90 to 90 Latitude coordinate for map center
longitude number -180 to 180 Longitude coordinate for map center
zoom number 0 to 22 Initial zoom level. Higher numbers = more zoomed in.

This only sets the initial map view. Once a search is performed, the map will automatically adjust to show search results.

mapOptions.mapboxConfig

Pass advanced Mapbox GL JS configuration options directly to the map instance. This allows fine-grained control over map behavior beyond what's available in the dashboard settings.

These options override dashboard settings, making them useful for one-off customizations or specific embedding contexts.

JavaScript
var storepoint = new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', {
    mapOptions: {
        mapboxConfig: {
            style: 'mapbox://styles/your-username/custom-style-id',  // Custom map style
            minZoom: 5,                    // Minimum zoom level
            maxZoom: 18,                   // Maximum zoom level
            maxBounds: [                   // Restrict panning to specific region
                [-10.0, 45.0],             // Southwest coordinates [lng, lat]
                [10.0, 60.0]               // Northeast coordinates [lng, lat]
            ],
            center: [-0.1278, 51.5074],    // Initial center [lng, lat]
            zoom: 12,                      // Initial zoom
            pitch: 0,                      // Map tilt angle (0-85)
            bearing: 0,                    // Map rotation (0-360)
            maxPitch: 60,                  // Maximum tilt angle
            minPitch: 0                    // Minimum tilt angle
        }
    }
});
Common Use Cases

Restrict map to a specific region (e.g., Europe only)

JavaScript
mapOptions: {
    mapboxConfig: {
        maxBounds: [
            [-12.0, 35.0],  // Southwest: West of Portugal, South of Gibraltar
            [40.0, 72.0]    // Northeast: East of Urals, North of Scandinavia
        ],
        minZoom: 3
    }
}

Use a custom Mapbox style

JavaScript
mapOptions: {
    mapboxConfig: {
        style: 'mapbox://styles/your-username/ckl6m2k7s0t7w17nzqhgj5k8m'
    }
}

Limit zoom levels

JavaScript
mapOptions: {
    mapboxConfig: {
        minZoom: 6,   // Users can't zoom out beyond country-level
        maxZoom: 16   // Users can't zoom in beyond neighborhood-level
    }
}
Reserved Properties

The following properties cannot be overridden as they're managed internally by the widget:

  • container - Widget manages its own container
  • accessToken - Managed by your dashboard settings

For a complete list of available options, see the Mapbox GL JS Map documentation.

Note: mapOptions.initialFocus and mapOptions.mapboxConfig apply only to Mapbox-powered maps. initialState, defaultView, and mapOptions.disabled work with all map providers.

Available Events

Search Events

search

Triggered when a search is performed.

JavaScript
Storepoint.on('search', function(data) {
    // data contains:
    // {
    //   lat: number|null,        // Latitude of search point
    //   lng: number|null,        // Longitude of search point
    //   query: string,           // Search text
    //   type: string,            // Search type (e.g., 'initial_search', 'filter_change')
    //   storepoint_location_id: number|null,  // ID of nearest location
    //   distance: number|null,   // Distance to nearest location
    //   tags: string[]|null      // Active filter tags
    // }
});

Location Events

location-result-item-rendered

Triggered when a location item is rendered in the results list and is available in the DOM.

When it fires:

  • After a location item appears in the visible portion of the results list
  • Only fires once per location per search session
  • Works with virtual scrolling - fires as items scroll into view
  • Fires after "Show More" is clicked on mobile
JavaScript
Storepoint.on('location-result-item-rendered', function(data) {
    // data contains:
    // {
    //   location: {
    //     id: "location_id",
    //     name: "Location Name",
    //     address: "123 Main St",
    //     // ... all other location properties
    //   },
    //   element: HTMLElement  // The DOM element containing the location card
    // }
    
    // Example: Inject custom content into the location card
    const infoDiv = data.element.querySelector('.storepoint-location-info');
    const customDiv = document.createElement('div');
    customDiv.innerHTML = '<strong>Custom info for ' + data.location.name + '</strong>';
    infoDiv.appendChild(customDiv);
});

This event is particularly useful for injecting dynamic content (like booking availability, real-time pricing, etc.) into location cards as they appear.

location-detail-panel-opened

Triggered when the location detail panel opens with a location (only fires when detail panel mode is enabled in your widget settings).

When it fires:

  • When a user clicks a location or map marker and the detail panel opens
  • Only when location_detail_panel_enabled setting is true in your dashboard
JavaScript
Storepoint.on('location-detail-panel-opened', function(data) {
    // data contains:
    // {
    //   location: {
    //     id: "location_id",
    //     name: "Location Name",
    //     address: "123 Main St",
    //     // ... all other location properties
    //   },
    //   element: HTMLElement  // The detail panel content container
    // }
    
    // Example: Inject additional details into the panel
    const infoDiv = data.element.querySelector('.storepoint-location-info');
    const detailDiv = document.createElement('div');
    detailDiv.className = 'custom-details';
    detailDiv.innerHTML = '<strong>Store Manager:</strong> ' + data.location.manager_name;
    infoDiv.appendChild(detailDiv);
});

location-result-item-click

Triggered when a location in the results list is clicked.

JavaScript
Storepoint.on('location-result-item-click', function(data) {
    // data contains location details
});

map-marker-click

Triggered when a location marker on the map is clicked.

JavaScript
Storepoint.on('map-marker-click', function(data) {
    // data contains location details
});

Online Store Events

online-store-click

Triggered when an online store link is clicked.

JavaScript
Storepoint.on('online-store-click', function(data) {
    // data contains:
    // {
    //   store_name: string,  // Name of the online store
    //   store_link: string   // URL of the online store
    // }
});

Widget Events

load

Triggered when the widget initially loads.

JavaScript
Storepoint.on('load', function(data) {
    // data contains:
    // {
    //   url: string,      // Current page URL
    //   referrer: string  // Referrer URL
    // }
});

no-results

Triggered when a search returns no results.

JavaScript
Storepoint.on('no-results', function(data) {
    // Useful for implementing fallback behavior
    window.location.href = '/store-not-found';
});

API Methods

Two ways to talk to the widget

Pick the one that matches how you embedded the locator. Both let you subscribe to events and call widget methods.

Standard embed

Pasted the embed code from your dashboard? Use the global Storepoint object. It's always available, even before the widget loads.

Storepoint.on('search', handler);
Storepoint.setFilters(['premium']);

React, Vue, modals, SPAs

Mounting the widget through code? Keep the instance you create and call methods on it. Cleanup is explicit. See Lifecycle.

const widget = new StorepointWidget(...);
widget.on('search', handler);
widget.setFilters(['premium']);

Storepoint.on(event, callback)

Subscribes to widget events. Can be called anywhere on your page - the callback will fire when the event occurs.

JavaScript
// Listen for search events
Storepoint.on('search', function(data) {
    console.log('Search performed');
});

// Listen for location clicks
Storepoint.on('location-result-item-click', function(data) {
    console.log('Location clicked:', data.name);
});

Timing-Safe: Storepoint.on() can be called before the widget loads. Event listeners are automatically queued and registered when the widget is ready.

Lifecycle (load, destroy, on, off)

Use the widget instance whenever the container can come and go: single-page apps, modals, popups, route changes, React StrictMode, anywhere the DOM mounts and unmounts.

The pattern is always the same: create, load, destroy.

JavaScript
const widget = new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', {});
widget.load();

widget.on('search', function (data) {
    console.log('search:', data.query);
});

// later, when the container goes away
widget.destroy();

widget.load()

Mounts the widget into its container. Safe to call again on the same instance to remount cleanly, for example after a modal re-creates its DOM, a route change, or React StrictMode's double-invoke.

widget.destroy()

Tears down the widget: unmounts the UI, removes its event listeners, and clears the container. Call this whenever the host element is going away.

widget.on(event, callback)

Subscribes to an event on this widget instance. Listeners added here are removed automatically by widget.destroy(), so cleanup never leaks across mounts.

widget.off(event, callback)

Removes a previously registered listener, if you need to detach one before destroy.

Example: popup or modal

Create the widget when the modal opens, destroy it when it closes. The embed script from your install snippet only needs to be on the page once.

JavaScript
let widget = null;

function openLocator() {
    document.getElementById('modal').classList.add('open');

    widget = new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', {});
    widget.load();
}

function closeLocator() {
    document.getElementById('modal').classList.remove('open');

    if (widget) {
        widget.destroy();
        widget = null;
    }
}

Building in React or Next.js? Follow the React & Next.js guide for a ready-to-use hook. If you only have the global API available, Storepoint.destroy() works the same way.

Storepoint.setLanguage(language)

Sets the widget's display language using a two-letter ISO language code (e.g. 'en', 'es', 'fr'). Must be called after widget loads.

JavaScript
// Call inside load callback or in response to user action
Storepoint.on('load', function() {
    Storepoint.setLanguage('fr');
});

// Or on a button click
document.getElementById('lang-btn').addEventListener('click', function() {
    Storepoint.setLanguage('es');
});

Storepoint.setFilters(filters)

Programmatically sets active tag filters on the widget. Must be called after widget loads.

JavaScript
// Filter to show only 'premium' locations on button click
document.getElementById('premium-btn').addEventListener('click', function() {
    Storepoint.setFilters(['premium']);
});

// Clear all filters
document.getElementById('clear-btn').addEventListener('click', function() {
    Storepoint.setFilters([]);
});

Storepoint.getCurrentLocations()

Returns an array of location objects currently visible in the widget. Must be called after widget loads.

JavaScript
// Get locations after widget loads
Storepoint.on('load', function() {
    var locations = Storepoint.getCurrentLocations();
    console.log('Loaded', locations.length, 'locations');
});

// Or get locations after a search
Storepoint.on('search', function() {
    var locations = Storepoint.getCurrentLocations();
    console.log('Found', locations.length, 'locations');
});

This method returns the current set of locations after any filtering or searching has been applied. The returned array contains the full location objects with all properties.

Storepoint.addCustomLayer(layerId, layerOptions)

Adds a custom GeoJSON layer to the map. Must be called after widget loads.

JavaScript
// Add a custom polygon layer after widget loads
Storepoint.on('load', function() {
    Storepoint.addCustomLayer('my-custom-layer', {
        type: 'fill',
        fillColor: '#FF5733',
        fillOpacity: 0.2,
        strokeWidth: 2,
        strokeColor: '#FF5733',
        strokeOpacity: 0.8,
        data: {
            type: 'FeatureCollection',
            features: [{
                type: 'Feature',
                geometry: {
                    type: 'Polygon',
                    coordinates: [[
                        [-74.006, 40.713],
                        [-74.006, 40.717],
                        [-73.998, 40.717],
                        [-73.998, 40.713],
                        [-74.006, 40.713]
                    ]]
                }
            }]
        }
    });
});

The layerOptions object supports various styling properties including type (fill, line, symbol), fillColor, fillOpacity, strokeWidth, strokeColor, and strokeOpacity. The data property should contain valid GeoJSON.

Storepoint.removeCustomLayer(layerId)

Removes a previously added custom layer from the map by its ID.

JavaScript
Storepoint.removeCustomLayer('my-custom-layer');

Storepoint.addCustomFilter(filterConfig)

Adds a custom filter UI component to the widget. Must be called after widget loads. The filterConfig object defines the filter's ID, filtering logic, UI template, and event handling.

See a worked example — a calendar-style date-range filter on a date custom field — in the custom filter recipes on GitHub.

JavaScript
// Add a custom range filter after widget loads
Storepoint.on('load', function() {
    Storepoint.addCustomFilter({
    // Unique identifier for this filter
    id: 'price_range',
    
    // Filter type (always 'custom' for custom filters)
    type: 'custom',
    
    /**
     * Filtering function that determines which locations to display
     * @param {Array} locations - Array of locations matching the current query before custom filters
     * @param {any} filterValue - Current value of the filter (from defaultValue or updateValue)
     * @return {Array} - Filtered array of locations that should be displayed
     */
    filter: function(locations, filterValue) {
        if (!filterValue) return locations;
        
        const min = filterValue[0];
        const max = filterValue[1];
        
        return locations.filter(location => {
            // Custom logic to filter locations based on a property
            const price = parseFloat(location.custom_fields.text.find(
                field => field.name === 'price'
            ).value);
            return !isNaN(price) && price >= min && price <= max;
        });
    },
    
    /**
     * HTML template for the filter UI
     * Can be provided as a string or as a getter function: get template() { return '...'; }
     * This defines the visual structure of your custom filter component
     */
    template: `
        <div class="price-filter">
            <div class="filter-header">Price Range</div>
            <div class="range-inputs">
                <input type="number" class="min-input" placeholder="Min">
                <span>to</span>
                <input type="number" class="max-input" placeholder="Max">
            </div>
        </div>
    `,
    
    /**
     * Setup function to initialize the filter UI and handle events
     * @param {HTMLElement} container - The DOM element where your filter HTML is mounted
     * @param {Function} updateValue - Callback function that must be called with the new filter value. This new value will be passed to the filter function to filter locations
     */
    setup: function(container, updateValue) {
        const minInput = container.querySelector('.min-input');
        const maxInput = container.querySelector('.max-input');
        
        // Handle input changes
        function handleChange() {
            const min = parseFloat(minInput.value) || 0;
            const max = parseFloat(maxInput.value) || 1000;
            updateValue([min, max]);
        }
        
        minInput.addEventListener('input', handleChange);
        maxInput.addEventListener('input', handleChange);
        
        // Initialize with default values
        minInput.value = 0;
        maxInput.value = 1000;
        updateValue([0, 1000]);
    },
    
    // Default filter value
    defaultValue: [0, 1000],
    
    // Optional CSS styles for the filter
    styles: `
        .price-filter {
            padding: 10px;
            background: #f5f5f5;
            border-radius: 4px;
        }
        .filter-header {
            font-weight: bold;
            margin-bottom: 8px;
        }
        .range-inputs {
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .min-input, .max-input {
            width: 80px;
            padding: 6px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
    `
    });
});

Custom filters allow you to create advanced filtering UI with your own logic. The filter function receives locations and filter value, returning filtered results. The setup function initializes the UI and handles user interactions.

Advanced Examples

Adding Radius Circles Around Locations

This example demonstrates how to add radius circles around all locations currently in view. It combines getCurrentLocations(), addCustomLayer(), and event listening to create dynamic visualizations.

JavaScript
// Add radius circles around locations after each search
Storepoint.on('search', function() {
    var locations = Storepoint.getCurrentLocations();
    
    // Remove existing layer
    Storepoint.removeCustomLayer('radius-circles');
    
    // Create circle features for each location
    var features = locations.map(function(location) {
        return createCircleFeature(
            [location.loc_long, location.loc_lat],
            50 // radius in miles
        );
    });
    
    // Add the layer
    Storepoint.addCustomLayer('radius-circles', {
        type: 'fill',
        fillColor: '#000000',
        fillOpacity: 0.15,
        strokeWidth: 1,
        strokeColor: '#000000',
        strokeOpacity: 0.3,
        data: {
            type: 'FeatureCollection',
            features: features
        }
    });
});

// Helper: Create a circle as a GeoJSON polygon
function createCircleFeature(center, radiusInMiles) {
    var points = 64;
    var lng = center[0];
    var lat = center[1];
    var coordinates = [];
    
    var distanceX = radiusInMiles / (111.320 * Math.cos(lat * Math.PI / 180) * 0.621371);
    var distanceY = radiusInMiles / (110.574 * 0.621371);
    
    for (var i = 0; i < points; i++) {
        var angle = (i / points) * (2 * Math.PI);
        coordinates.push([
            lng + distanceX * Math.cos(angle),
            lat + distanceY * Math.sin(angle)
        ]);
    }
    coordinates.push(coordinates[0]); // Close the polygon
    
    return {
        type: 'Feature',
        geometry: { type: 'Polygon', coordinates: [coordinates] }
    };
}

This example creates radius circles around each location to visualize coverage areas. The circles are added as a custom layer using GeoJSON polygons. The radius calculation uses a simplified approximation that works well for most use cases.

Creating a Dual-Range Filter

This example shows how to create a sophisticated dual-range filter for filtering locations by a numeric range (such as square footage or price).

This is a more advanced example that demonstrates the flexibility of custom filters.

JavaScript
// Add a custom dual range filter after widget loads
Storepoint.on('load', function() {
    Storepoint.addCustomFilter({
        type: 'custom',
        id: 'sq_ft_range',
    
    // Filtering function
    filter: function(locations, filterValue) {
        if (!filterValue) return locations;
        
        const min = filterValue[0];
        const max = filterValue[1];
        
        return locations.filter(location => {
            // Find square footage values in custom fields
            let sqFt = null;
            
            // Check if custom_fields exist
            if (location.custom_fields && location.custom_fields.text) {
                // Find the square footage field by its token
                const sqFtField = location.custom_fields.text.find(
                    field => field.token === "square_footage_token"
                );
                
                if (sqFtField) {
                    sqFt = parseFloat(sqFtField.value);
                }
            }
            
            // If square footage exists and is in range, include the location
            if (!isNaN(sqFt)) {
                return sqFt >= min && sqFt <= max;
            }
            
            // If no square footage data, don't filter out
            return true;
        });
    },
    
    // Configuration for the filter
    rangeConfig: {
        minValue: 0,
        maxValue: 10000,
        defaultMin: 0,
        defaultMax: 10000,
        step: 100
    },
    
    // HTML template for the filter UI
    template: `
        <div class="range-filter">
            <div class="filter-header">Square Footage</div>
            <div class="range-inputs">
                <input type="number" class="min-input" placeholder="Min">
                <span>to</span>
                <input type="number" class="max-input" placeholder="Max">
                <span>sq ft</span>
            </div>
            <div class="range-slider">
                <input type="range" class="min-slider" min="0" max="10000" step="100" value="0">
                <input type="range" class="max-slider" min="0" max="10000" step="100" value="10000">
            </div>
        </div>
    `,
    
    // Setup function (simplified version)
    setup: function(container, updateValue) {
        const minInput = container.querySelector('.min-input');
        const maxInput = container.querySelector('.max-input');
        const minSlider = container.querySelector('.min-slider');
        const maxSlider = container.querySelector('.max-slider');
        
        // Handle input changes and update the filter value
        function handleChange() {
            const min = parseInt(minInput.value) || 0;
            const max = parseInt(maxInput.value) || 10000;
            updateValue([min, max]);
        }
        
        // Set up event listeners
        minInput.addEventListener('input', handleChange);
        maxInput.addEventListener('input', handleChange);
        minSlider.addEventListener('input', function() {
            minInput.value = this.value;
            handleChange();
        });
        maxSlider.addEventListener('input', function() {
            maxInput.value = this.value;
            handleChange();
        });
        
        // Initialize with default values
        minInput.value = 0;
        maxInput.value = 10000;
        updateValue([0, 10000]);
    },
    
    // Default filter value
    defaultValue: [0, 10000],
    
    // CSS styles for the filter (simplified)
    styles: `
        .range-filter {
            padding: 10px;
            background: #f5f5f5;
            border-radius: 4px;
        }
        .filter-header {
            font-weight: bold;
            margin-bottom: 8px;
        }
        .range-inputs {
            display: flex;
            align-items: center;
            gap: 8px;
            margin-bottom: 8px;
        }
        .min-input, .max-input {
            width: 80px;
            padding: 6px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .range-slider {
            position: relative;
            height: 30px;
        }
        .min-slider, .max-slider {
            position: absolute;
            width: 100%;
            pointer-events: none;
        }
        .min-slider::-webkit-slider-thumb, .max-slider::-webkit-slider-thumb {
            pointer-events: auto;
        }
    `
    });
});

Integrations

Google Analytics / Google Tag Manager

Track widget interactions in Google Analytics. This example sends events to gtag.

JavaScript
// Google Analytics GTM Events
// Add this anywhere on your page

Storepoint.on('load', function() {
    gtag('event', 'storepoint_widget_load');
});

Storepoint.on('search', function() {
    gtag('event', 'storepoint_search');
});

Storepoint.on('location-result-item-click', function() {
    gtag('event', 'storepoint_location_click', {
        click_source: 'list'
    });
});

Storepoint.on('map-marker-click', function() {
    gtag('event', 'storepoint_location_click', {
        click_source: 'map'
    });
});

Google Tag Manager (dataLayer)

If you're using Google Tag Manager, push events to the dataLayer instead:

JavaScript
Storepoint.on('load', function() {
    dataLayer.push({ event: 'storepoint_widget_load' });
});

Storepoint.on('search', function() {
    dataLayer.push({ event: 'storepoint_search' });
});

Storepoint.on('location-result-item-click', function() {
    dataLayer.push({ event: 'storepoint_location_click' });
});

Storepoint.on('map-marker-click', function() {
    dataLayer.push({ event: 'storepoint_location_click' });
});

To use these events in GTM, create a Custom Event trigger for each event name (e.g., storepoint_search) and connect it to your GA4 Event tag. See the GTM Custom Event documentation.

Local Testing Mode

Local testing mode lets you preview the Storepoint widget without creating an account. Pass 'local' as the widget ID, supply your own data source (CSV, JSON, or a published Google Sheet), and bring a Mapbox token or Google Maps API key. Useful for design mockups, internal demos, AI-generated previews, and quick experiments before connecting Storepoint to your production data.

More worked examples — store locator from CSV, store locator from JSON, product locator, service locator — in the data-sources recipes on GitHub. Map provider overrides live in the map-providers recipes.

Local testing mode is intended for development, mockups, and previews. It does not include Storepoint dashboard management, imports, geocoding, support, or production assistance. For production use, create a Storepoint account and use your public widget ID.

In local testing mode the widget does not send full Storepoint hosted analytics. Storepoint may still receive normal requests for widget scripts and assets, operational logs, and basic widget usage telemetry such as load host, widget version, provider type, local-token usage, and generic load/click events.

Storepoint does not manage, protect, rotate, monitor, or pay for third-party map provider keys used with local. Keep your data source public only if you intend visitors to access it.

Quick Start

A complete local testing setup. The widget loads CSV rows, normalizes them with your field map, applies your filters and settings, and renders with Mapbox.

JavaScript
var storepoint = new StorepointWidget('local', '#storepoint-widget', {
    dataSource: {
        type: 'csv',
        url: 'https://docs.google.com/spreadsheets/d/.../pub?output=csv'
    },
    fieldMap: {
        name: 'store_name',
        lat: 'latitude',
        lng: 'longitude',
        address: { columns: ['street', 'city', 'state', 'zip'], join: ', ' },
        phone: 'phone_number',
        website: 'store_url',
        image_url: 'photo',
        description: 'notes',
        tags: [
            { column: 'product_type' },
            { column: 'services', split: ',' }
        ]
    },
    settings: {
        accent_color: '#111111',
        search_radius_default: 25,
        search_radius_options: [10, 25, 50],
        filters: [
            {
                id: 'product_type',
                name: 'Product type',
                tags: [
                    {
                        tag: 'retail',
                        options: {
                            label: 'Retail',
                            color: '#111111',
                            image_url: 'https://example.com/images/retail.png'
                        }
                    },
                    {
                        tag: 'wholesale',
                        options: {
                            label: 'Wholesale',
                            color: '#5c5cd8',
                            image_url: 'https://example.com/images/wholesale.png'
                        }
                    }
                ]
            }
        ]
    },
    mapProvider: {
        use: 'mapbox',
        accessToken: 'pk_browser_restricted_token'
    }
});

storepoint.load();

Data Source

The dataSource option tells the widget where your locations come from. You can fetch from a URL or pass rows directly.

Property Type Description
type 'csv' | 'json' Format of the data. If omitted, the type is inferred from the URL extension or query (e.g. .csv, .json, or Google Sheets output=csv).
url string URL to fetch from the visitor's browser. Use a CORS-friendly endpoint (a published Google Sheet CSV link, a static file on your domain, or a public JSON file).
rows array Inline rows passed directly. Each row is an object whose keys match your data columns. Use this when locations live in your own JavaScript code or are generated at runtime.

CSV shape

A CSV needs at least latitude and longitude. Any other column becomes available to the field map.

CSV
store_name,street,city,state,zip,latitude,longitude,product_type,services,phone_number,store_url,photo,notes
Downtown Store,123 Main St,Austin,TX,78701,30.2672,-97.7431,retail,"pickup,repairs",555-1234,https://example.com,https://example.com/store.jpg,Flagship downtown location

Inline JSON example

JavaScript
var storepoint = new StorepointWidget('local', '#storepoint-widget', {
    dataSource: {
        type: 'json',
        rows: [
            {
                name: 'Downtown Store',
                address: '123 Main St, Austin, TX 78701',
                lat: 30.2672,
                lng: -97.7431,
                tags: ['retail', 'pickup'],
                phone: '555-1234',
                website: 'https://example.com',
                image_url: 'https://example.com/store.jpg'
            }
        ]
    },
    mapProvider: {
        use: 'google_maps',
        apiKey: 'browser_restricted_google_key'
    }
});

storepoint.load();

Field Mapping

The fieldMap option tells the widget how to read your data. Each entry maps a Storepoint field to one or more columns in your CSV or JSON. Common column names like name, lat, lng, tags, phone, website, image_url, and description are detected automatically when no explicit mapping is provided.

Field Accepts Description
name string Column name to use as the location's display name.
lat / lng string Column names for latitude and longitude. Required. Rows missing valid coordinates are skipped with a console warning.
address string | object Either a single column name, or an object that combines multiple columns: { columns: ['street', 'city', 'state', 'zip'], join: ', ' }.
phone string Column name for phone number. The widget formats numbers automatically.
website string Column name for website URL. Renders as a button or link in the location card.
image_url string Column name for a location photo URL. Displayed at the top of each location card when present.
description string Column name for a longer description block under the location details.
tags string | object | array Where this location's tags come from. Accepts a single column name, an object { column, split }, or an array of column rules to gather tags from multiple columns.
monday – sunday string Column names for store hours per day, e.g. '9am - 5pm'. Optional.

Any column that isn't part of the field map automatically becomes a custom text field on the location card, so you can drop in extra columns without configuring them explicitly.

Combining columns

Combine multiple columns into a single value. The widget joins each value with the separator and skips empty cells.

JavaScript
fieldMap: {
    address: {
        columns: ['street', 'city', 'state', 'zip'],
        join: ', '
    }
}

Tag sources

Tags can come from a single column, multiple columns, or a column with comma-separated values. Each entry in the array becomes part of the same internal tag list.

JavaScript
fieldMap: {
    // Multiple tag columns. The "services" cell may contain comma-separated values.
    tags: [
        { column: 'product_type' },
        { column: 'services', split: ',' }
    ]
}

split means "this single cell may contain multiple tag values." It does not create separate filter groups; it simply expands one cell into multiple tags.

Settings & Filters

The settings option overrides the widget's defaults. In local testing mode, only client-side settings apply (filters, search radius, theming, layout). Server-managed features such as spotlight, online alternatives, and service areas are not available without a Storepoint account.

Common settings

Setting Type Description
accent_color string Hex color used for buttons, links, and accents.
theme 'default' | 'brand' | 'brand-bold' | 'dark' | 'brand-dark' Visual theme. brand uses your accent color on subtle accents; brand-bold applies it more broadly.
layout 'default' | 'stacked' | 'sidebyside' | 'floating' Controls how the search panel, results list, and map are arranged.
search_radius_default number Default search radius applied on first load.
search_radius_options number[] Radius choices that appear in the radius dropdown.
distance_unit 'auto' | 'mi' | 'km' Use miles, kilometers, or auto-detect from the visitor's locale.
enable_keyword_search boolean Show a separate keyword search field. Filters by name, address, tags, and description.
show_geolocation_button boolean Show a "use my location" button next to the search bar.
hide_filters boolean Hide all filter dropdowns regardless of filters config.
location_initial_load_count number Number of locations rendered before scrolling triggers more.
filters array Filter groups shown above the results. See the next section.

Filters

Each filter is a group of related tags. The id identifies the group, the name appears as the dropdown title, and each tag references a value found in your data. Tags accept rich options for label, color, and image so previews look polished.

Tag option Type Description
label string Human-readable label shown in the filter dropdown and on tag chips.
color string Background color for the tag chip. Hex value.
image_url string Optional product or service image rendered next to the tag label.
JavaScript
settings: {
    filters: [
        {
            id: 'product_type',
            name: 'Product type',
            tags: [
                {
                    tag: 'retail',
                    options: {
                        label: 'Retail',
                        color: '#1f5f3b',
                        image_url: 'https://example.com/retail.png'
                    }
                },
                {
                    tag: 'wholesale',
                    options: {
                        label: 'Wholesale',
                        color: '#5c5cd8',
                        image_url: 'https://example.com/wholesale.png'
                    }
                }
            ]
        },
        {
            id: 'services',
            name: 'Services',
            tags: [
                { tag: 'pickup', options: { label: 'Pickup' } },
                { tag: 'repairs', options: { label: 'Repairs' } }
            ]
        }
    ]
}

Map Providers

Local testing mode uses your map provider key in the visitor's browser for map loads and search/autocomplete requests. Browser keys are visible in frontend code, so always restrict them.

Mapbox

Use a public browser token only (prefixed pk_). Do not use secret tokens. Create a separate token for this project and add URL restrictions before sharing the page beyond local development.

Map loads, tile/style requests, and Mapbox Geocoding API/search suggestion requests count toward your Mapbox usage and billing.

Google Maps

Use a browser-restricted API key. Restrict it to your site domains and only the APIs the widget uses (Maps JavaScript API and Places API for search suggestions).

Google Maps JavaScript map loads/views and Places/search suggestion requests count toward your Google Maps Platform usage and billing.

JavaScript
// Mapbox
mapProvider: {
    use: 'mapbox',
    accessToken: 'pk_browser_restricted_token'
}

// Google Maps
mapProvider: {
    use: 'google_maps',
    apiKey: 'browser_restricted_google_key'
}

Going Live

When your local testing setup is working, you can launch the same locator on a real website by replacing 'local' with your public Storepoint widget ID.

JavaScript
// Local testing
new StorepointWidget('local', '#storepoint-widget', { /* dataSource, fieldMap, settings, mapProvider */ });

// Production
new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', { /* optional embed overrides */ });

Two ways to manage your locator

Once you have a Storepoint widget ID, you can keep configuring everything in code (great if you already have a working embed and want full control), or move pieces of the configuration into your dashboard.

Configuration Configure in embed code Configure in Storepoint dashboard
Locations Pass dataSource with your CSV/JSON/Sheet URL. Remove dataSource and use Storepoint's hosted location data, imports, and geocoding.
Filters & tags Pass settings.filters with rich tag options (label, color, image). Configure tag groups and tag images in your Storepoint dashboard.
Theme & layout Pass settings.theme, settings.accent_color, settings.layout, etc. Set theme, colors, and layout in the dashboard once and reuse across embeds.
Map provider Pass mapProvider with your Mapbox token or Google Maps key. Add your provider key in the dashboard so every embed inherits it.

Embed options take precedence over dashboard settings. Anything you pass into the constructor (like settings.filters or mapProvider) overrides the matching dashboard configuration for that page.

When to move to dashboard-managed data

Many teams keep their embed code as-is when launching, especially when their data already lives in a CSV or Google Sheet. Dashboard-managed data is the easier path when you want any of the following:

  • Manage locations in a spreadsheet-style interface without code changes.
  • Bulk import locations and have addresses geocoded automatically.
  • Let non-technical teammates edit hours, tags, and details.
  • Share the same locations across multiple embeds and pages.
  • Use Storepoint analytics, online alternatives, spotlights, and service areas.
  • Get Storepoint support for your locator setup.

To use dashboard-managed data, simply remove the dataSource option from your embed. The widget will use the locations and settings configured in your Storepoint account.

Ready to launch?

Create a Storepoint account, replace 'local' with your public widget ID, and ship.

Sign up for Storepoint

Interested in other events or functions?

We're constantly expanding our API capabilities. Contact us to discuss your specific needs or request new features.

Contact us at [email protected]