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. 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.
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.
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.
var storepoint = new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', {
defaultView: 'none'
});
Combine with mapOptions.disabled to create a search-first experience with a full-width results list.
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.
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.
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.
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; }
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: mapOptions.initialFocus and mapOptions.mapboxConfig apply only to Mapbox-powered maps. initialState, defaultView, and mapOptions.disabled work 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';
});
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']);
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.
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.
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();
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.
Tears down the widget: unmounts the UI, removes its event listeners, and clears the container. Call this whenever the host element is going away.
Subscribes to an event on this widget instance. Listeners added here are removed automatically by widget.destroy(), so cleanup never leaks across mounts.
Removes a previously registered listener, if you need to detach one before destroy.
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.
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.
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.
See a worked example — a calendar-style date-range filter on a date custom field — in the custom filter recipes on GitHub.
// 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.
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.
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.
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();
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. |
A CSV needs at least latitude and longitude. Any other column becomes available to the field map.
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
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();
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.
Combine multiple columns into a single value. The widget joins each value with the separator and skips empty cells.
fieldMap: {
address: {
columns: ['street', 'city', 'state', 'zip'],
join: ', '
}
}
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.
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.
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.
| 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. |
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. |
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' } }
]
}
]
}
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.
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.
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.
// Mapbox
mapProvider: {
use: 'mapbox',
accessToken: 'pk_browser_restricted_token'
}
// Google Maps
mapProvider: {
use: 'google_maps',
apiKey: 'browser_restricted_google_key'
}
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.
// Local testing
new StorepointWidget('local', '#storepoint-widget', { /* dataSource, fieldMap, settings, mapProvider */ });
// Production
new StorepointWidget('YOUR_PUBLIC_WIDGET_ID', '#storepoint-widget', { /* optional embed overrides */ });
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.
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:
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.
Create a Storepoint account, replace 'local' with your public widget ID, and ship.
We're constantly expanding our API capabilities. Contact us to discuss your specific needs or request new features.
Contact us at [email protected]