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 →Find your unique embed code (and public widget ID) on the 'Embed Locator' page in your dashboard.
Get Embed CodeCopy the embed code from your dashboard and paste it into your website:
<!-- 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.
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.
Pass configuration options as the third parameter when initializing the widget to customize behavior and override dashboard settings.
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.
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
}
});
| 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. |
Pre-fill search with coordinates
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
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.
Advanced 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.
Set the initial map viewport before any search is performed. Useful for centering the map on a specific region, city, or country.
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.
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.
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
}
}
});
Restrict map to a specific region (e.g., Europe only)
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
mapOptions: {
mapboxConfig: {
style: 'mapbox://styles/your-username/ckl6m2k7s0t7w17nzqhgj5k8m'
}
}
Limit zoom levels
mapOptions: {
mapboxConfig: {
minZoom: 6, // Users can't zoom out beyond country-level
maxZoom: 16 // Users can't zoom in beyond neighborhood-level
}
}
The following properties cannot be overridden as they're managed internally by the widget:
container - Widget manages its own containeraccessToken - Managed by your dashboard settingsFor a complete list of available options, see the Mapbox GL JS Map documentation.
Note: These configuration options apply only to Mapbox-powered maps. If you're using Google Maps as your map provider, these options won't apply (except initialState, which works with all map providers).
Triggered when a search is performed.
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
// }
});
Triggered when a location item is rendered in the results list and is available in the DOM.
When it fires:
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.
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:
location_detail_panel_enabled setting is true in your dashboardStorepoint.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);
});
Triggered when a location in the results list is clicked.
Storepoint.on('location-result-item-click', function(data) {
// data contains location details
});
Triggered when a location marker on the map is clicked.
Storepoint.on('map-marker-click', function(data) {
// data contains location details
});
Triggered when an online store link is clicked.
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
// }
});
Triggered when the widget initially loads.
Storepoint.on('load', function(data) {
// data contains:
// {
// url: string, // Current page URL
// referrer: string // Referrer URL
// }
});
Triggered when a search returns no results.
Storepoint.on('no-results', function(data) {
// Useful for implementing fallback behavior
window.location.href = '/store-not-found';
});
Subscribes to widget events. Can be called anywhere on your page - the callback will fire when the event occurs.
// 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.
Sets the widget's display language using a two-letter ISO language code (e.g. 'en', 'es', 'fr'). Must be called after widget loads.
// 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');
});
Programmatically sets active tag filters on the widget. Must be called after widget loads.
// 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([]);
});
Returns an array of location objects currently visible in the widget. Must be called after widget loads.
// 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.
Adds a custom GeoJSON layer to the map. Must be called after widget loads.
// 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.
Removes a previously added custom layer from the map by its ID.
Storepoint.removeCustomLayer('my-custom-layer');
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.
// 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.
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.
// 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.
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.
// 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;
}
`
});
});
Track widget interactions in Google Analytics. This example sends events to gtag.
// 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'
});
});
If you're using Google Tag Manager, push events to the dataLayer instead:
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.
We're constantly expanding our API capabilities. Contact us to discuss your specific needs or request new features.
Contact us at [email protected]