How to Build Dynamic, CMS-Powered Google Maps in Webflow
CMS Powered maps in webflow With Custom Styling and location filtering

Enthusiastic and dedicated Full Stack Developer with a passion for crafting efficient, user-centric, and innovative web solutions. Since 2019 I’ve been providing high-level support to agencies, startups and freelancing in various positions.
If you're building a location directory, a store locator, or any type of interactive map in Webflow, you’ve probably noticed that Webflow has no native support for dynamic Google Maps. But with a bit of JavaScript and a properly structured CMS collection, you can create a fully dynamic, CMS-powered Google Map, complete with custom markers, hover states, tooltips, and a modal that displays extra details for each location.
This guide walks you through:
Structuring your Webflow CMS for map data
Binding CMS fields to custom HTML attributes
Loading all locations dynamically
Displaying markers with hover + click interactions
Showing a modal with rich details
Adding your own Google Maps Map ID for custom map styling
Filtering the locations via search input
Let’s get started.
1. Create Your Webflow CMS Structure
Your CMS collection will store everything the map needs: coordinates, image, phone, address, etc.
A typical Centers CMS item contains:
| Field Name | Purpose | Example |
| Name | Marker title | “Valley Forge Medical Center” |
| Latitude | Google Maps latitude | 40 |
| Longitude | Google Maps longitude | -75 |
| Address | Full location address | “657 Valley Forge Rd…” |
| Phone | Contact phone number | (610) 555-0349 |
| Fax | Optional extra field | (610) 555-0350 |
| Image | Photo displayed in the modal | JPG/PNG |
| Image URL (optional) | Direct asset URL | Webflow CDN URL |
Example CMS item (from Webflow editor)
Your CMS is structured like this:
Name: Valley Forge Medical Center
Latitude: 40
Longitude: -75
Address: 657 Valley Forge Rd, King of Prussia, PA 19406
Phone: (610) 555-0349
Fax: (610) 555-0350
Image: Uploaded JPG
Image URL: Webflow CDN link
This data must be exposed to the frontend so your JavaScript can use it.
2. Create your HTML Strucure

This solution is attribute based. This is the complete HTML structure powering the interactive map, CMS pins, and modal system. Each element includes its role and important attributes such as dev-target, pin metadata, image attributes, and Webflow system classes.
<div dev-target="map-wrapper" class="map-wrapper">
<!-- Modal -->
<div dev-target="map-modal" class="map-modal">
<div class="map-modal_contents">
<div class="map-image_wrapper">
<img
class="map-location_image"
src="https://cdn.prod.website-files.com/691c9ee31e5ce242565c9e48/692857c057e429999861abf6_image-pinned.png"
alt=""
dev-target="map-modal-image"
loading="lazy"
>
</div>
<div class="modal-detail_wrapper">
<div dev-target="map-modal-title" class="modal-header"></div>
<div dev-target="map-modal-address" class="modal-address"></div>
<div class="modal-phone_wrapper">
<div dev-target="map-modal-phone" class="modal-contact"></div>
<div dev-target="map-modal-fax" class="modal-contact"></div>
</div>
</div>
</div>
</div>
<!-- CMS Pins -->
<div dev-target="cms-collection-wrapper" class="cms-data">
<div class="w-dyn-list">
<div role="list" class="w-dyn-items">
<!-- Your CMS items will auto-render here -->
</div>
</div>
</div>
<!-- Placeholder map image -->
<img
src="https://cdn.prod.website-files.com/691c9ee31e5ce242565c9e48/692856ba201c7cc04f9ba288_map-pins.png"
loading="lazy"
alt=""
class="map-image_placeholder"
>
</div>
<!-- Map container -->
<div dev-target="map-content" class="map-content"></div>
</div>
3. Bind CMS Fields to HTML Attributes
Inside your Webflow Collection List, add a wrapper element and attach custom attributes that your script will read:
<div
dev-target="cms-pin"
pin-name="{{Name}}"
pin-image="{{image.url}}"
pin-latitude="{{Latitude}}"
pin-longitude="{{Longitude}}"
pin-address="{{Address}}"
pin-phone="{{Phone}}"
pin-fax="{{Fax}}"
></div>
Each {{Field}} is bound using Webflow’s purple CMS field picker.
Now, each CMS item becomes a “pin object” that the script can extract when building the map.
3. Add Your Map Container + Modal in Webflow
You need:
Map Container
A div with:
dev-target="map-content"
Modal
A hidden modal that shows the center details on marker click:
dev-target="map-modal"
dev-target="map-modal-title"
dev-target="map-modal-image"
dev-target="map-modal-address"
dev-target="map-modal-phone"
dev-target="map-modal-fax"
Your JavaScript will dynamically fill these elements.
4. Add the JavaScript to Load CMS Pins + Build the Map
Here is the full working script, including hover, tooltip, modal interactions, and map initialization:
document.addEventListener('DOMContentLoaded', () => {
function readCmsPins() {
const elements = document.querySelectorAll('[dev-target="cms-pin"]');
const pins = [];
elements.forEach((el) => {
const pin = {
name: el.getAttribute("pin-name") || "",
image: el.getAttribute("pin-image") || "",
latitude: parseFloat(el.getAttribute("pin-latitude") || "0"),
longitude: parseFloat(el.getAttribute("pin-longitude") || "0"),
address: el.getAttribute("pin-address") || "",
phone: el.getAttribute("pin-phone") || "",
fax: el.getAttribute("pin-fax") || ""
};
pins.push(pin);
});
return pins;
}
const showCenterModal = (center) => {
const modal = document.querySelector("[dev-target='map-modal']");
modal.classList.add("show");
modal.querySelector("[dev-target='map-modal-title']").textContent = center.name;
modal.querySelector("[dev-target='map-modal-image']").src = center.image;
modal.querySelector("[dev-target='map-modal-address']").textContent = center.address;
modal.querySelector("[dev-target='map-modal-phone']").textContent = center.phone;
modal.querySelector("[dev-target='map-modal-fax']").textContent = center.fax;
};
function initMap() {
const centers = readCmsPins();
const mapEl = document.querySelector("[dev-target='map-content']");
const map = new google.maps.Map(mapEl, {
zoom: 2,
center: { lat: 20, lng: 0 },
mapTypeControl: false,
streetViewControl: false,
mapId: "YOUR_MAP_ID_HERE" // Custom map styling from the cloud
});
const blackPin = "https://cdn.../black-pointer.png";
const bluePin = "https://cdn.../blue-pointer.png";
const bounds = new google.maps.LatLngBounds();
centers.forEach((center) => {
const marker = new google.maps.Marker({
position: { lat: center.latitude, lng: center.longitude },
map,
icon: {
url: blackPin,
scaledSize: new google.maps.Size(20, 20)
},
title: center.name
});
bounds.extend(marker.getPosition());
const infoWindow = new google.maps.InfoWindow({
content: `<div style="font-size:14px;font-weight:600;">${center.name}</div>`
});
marker.addListener("click", () => showCenterModal(center));
marker.addListener("mouseover", () => {
infoWindow.open({ map, anchor: marker });
marker.setIcon({
url: bluePin,
scaledSize: new google.maps.Size(25, 25),
});
});
marker.addListener("mouseout", () => {
infoWindow.close();
marker.setIcon({
url: blackPin,
scaledSize: new google.maps.Size(20, 20),
});
});
});
map.fitBounds(bounds);
map.addListener("click", () => {
document.querySelector("[dev-target='map-modal']").classList.remove("show");
});
}
window.initMap = initMap;
const script = document.createElement("script");
script.src = "https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap";
script.async = true;
document.head.appendChild(script);
});
Replace:
YOUR_MAP_ID_HEREYOUR_API_KEY
with your real values.
5. Adding a Custom Map Style Using a Google Maps Map ID
Google now recommends using cloud-based styling for maps. Instead of adding JSON styling in your JavaScript, you create a style in the Google Cloud console and attach it to a Map ID.
This gives you huge advantages:
✔ Update map styling without touching code
✔ Switch between light/dark modes
✔ Maintain all styling in the cloud
✔ Instant global updates
Here’s how to set it up.
Step 1 — Create a Map ID
Log into Google Cloud Console.
Go to Maps Management → Map IDs.
Click Create Map ID.
Enter name + description.
Choose Platform: JavaScript.
Choose Map Type: Map.
Click Save.
You’ll get something like:
0123abcd4567ef89
Step 2 — Create & Associate a Map Style
In Maps Management, click your new Map ID.
Scroll to Map styles.
Choose a mode (Light / Dark / Auto).
Click Edit.
Create a style by:
Using the Google Maps Styling Wizard, or
Pasting JSON style code
{ "styles": [ { "id": "natural.water", "geometry": { "fillColor": "#b2d2de" } }, { "id": "natural.land", "geometry": { "fillColor": "#f2efea" } }, { "id": "infrastructure.roadNetwork", "geometry": { "visible": true }, "label": { "visible": true } }, { "id": "pointOfInterest", "geometry": { "visible": false }, "label": { "visible": false } } ] }
Click Done → Save.
Google Maps now includes a built-in visual styling editor inside the Cloud Console. You can customize sea colors, land, roads, labels, POIs, borders, and UI elements directly.

No need to write or upload a JSON file. Map settings let you define map style properties, such as building and landmark styles and POI density levels. Styleable elements are all shown on a single panel, giving you more visible map area when making customizations. Find your map feature with more intuitive categories that expand and collapse, plus a filter box to scope the list to the map feature you need. You can also click anywhere on the map and see what features you can style at that location using the map inspector.
Your style is now linked to the Map ID.
Step 3 — Apply the Map ID in Your Webflow Map Script
Just add the mapId property:
const map = new google.maps.Map(mapEl, {
zoom: 2,
center: { lat: 20, lng: 0 },
mapTypeControl: false,
streetViewControl: false,
mapId: "YOUR_MAP_ID_HERE"
});
That’s it — the map now uses your cloud-hosted custom design.
6. Location Filtering
One of the most useful features in any location-based interface is the ability to quickly filter locations based on what the user is looking for. In our custom Google Map implementation, we built a dynamic filtering system that works directly with CMS items. Our map includes a search bar that lets users quickly find locations by typing a city, state, ZIP code, or any part of an address. The filtering is simple and works in three steps:
1. Read All Locations From the CMS
Each CMS item has attributes like name, address, latitude, and longitude.
The script collects them into an array called centers.
This becomes the full list of map locations.
2. Listen to Search Input
When the user types into the search field, the script checks what they entered.
If the search box is empty → all locations are shown again.
3. Filter Based on Address
Every location has one long address string.
The script just checks:
“Does this address contain the text the user typed?”
If yes → keep the pin.
If no → hide it.
4. Re-Render Markers
After filtering, the script clears old markers and shows only the matched ones.
The map automatically zooms to the remaining pins.
document.addEventListener('DOMContentLoaded', () => {
/** -------------------------------------------
* Read CMS pins from Webflow attributes
* ------------------------------------------- */
function readCmsPins() {
const elements = document.querySelectorAll('[dev-target="cms-pin"]');
const pins = [];
elements.forEach((el) => {
pins.push({
name: el.getAttribute("pin-name") || "",
image: el.getAttribute("pin-image") || "",
latitude: parseFloat(el.getAttribute("pin-latitude") || "0"),
longitude: parseFloat(el.getAttribute("pin-longitude") || "0"),
address: el.getAttribute("pin-address") || "",
phone: el.getAttribute("pin-phone") || "",
fax: el.getAttribute("pin-fax") || ""
});
});
return pins;
}
/** -------------------------------------------
* Modal handling
* ------------------------------------------- */
const showCenterModal = (center) => {
const modal = document.querySelector("[dev-target='map-modal']");
if (!modal) return;
modal.classList.add("show");
modal.querySelector("[dev-target='map-modal-title']").textContent = center.name;
modal.querySelector("[dev-target='map-modal-image']").src = center.image;
modal.querySelector("[dev-target='map-modal-address']").textContent = center.address;
modal.querySelector("[dev-target='map-modal-phone']").textContent = center.phone;
modal.querySelector("[dev-target='map-modal-fax']").textContent = center.fax;
};
/** -------------------------------------------
* Haversine distance between two lat/lng points
* ------------------------------------------- */
function haversineDistance(loc1, loc2) {
const R = 6371; // km
const dLat = (loc2.lat - loc1.lat) * Math.PI / 180;
const dLng = (loc2.lng - loc1.lng) * Math.PI / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(loc1.lat * Math.PI / 180) *
Math.cos(loc2.lat * Math.PI / 180) *
Math.sin(dLng / 2) *
Math.sin(dLng / 2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
/** -------------------------------------------
* Main Map Initialization
* ------------------------------------------- */
function initMap() {
const centers = readCmsPins();
const mapEl = document.querySelector("[dev-target='map-content']");
if (!mapEl) {
console.error("Map element missing");
return;
}
/** Google Map */
const map = new google.maps.Map(mapEl, {
zoom: 2,
center: { lat: 20, lng: 0 },
mapTypeControl: false,
streetViewControl: false,
mapId: "XXXXXXXXXX",
});
/** Marker icons */
const blackPin = "https://cdn.prod.website-files.com/691c9ee31e5ce242565c9e48/69286da97ef7df8fa444541a_black-pointer-svg.svg";
const bluePin = "https://cdn.prod.website-files.com/691c9ee31e5ce242565c9e48/69286da940c210bff4650642_blue-pointer-svg.svg";
let markers = [];
/** -------------------------------------------
* Render markers on map
* ------------------------------------------- */
const renderMarkers = (pins = []) => {
// Remove old markers
markers.forEach(marker => marker.map = null);
markers = [];
if (pins.length === 0) {
map.setZoom(2);
map.setCenter({ lat: 20, lng: 0 });
return;
}
const bounds = new google.maps.LatLngBounds();
pins.forEach(center => {
const iconEl = document.createElement("img");
iconEl.src = blackPin;
iconEl.style.width = "38px";
iconEl.style.height = "54px";
iconEl.style.transition = "0.2s";
const marker = new google.maps.marker.AdvancedMarkerElement({
map,
position: { lat: center.latitude, lng: center.longitude },
title: center.name,
content: iconEl
});
bounds.extend(marker.position);
/** Hover tooltip */
const infoWindow = new google.maps.InfoWindow({
content: `<div style="font-size:16px;">${center.name}</div>`
});
marker.element.addEventListener("mouseenter", () => {
infoWindow.open({ map, anchor: marker });
iconEl.src = bluePin;
iconEl.style.transform = "scale(1.1)";
});
marker.element.addEventListener("mouseleave", () => {
infoWindow.close();
iconEl.src = blackPin;
iconEl.style.transform = "scale(1)";
});
/** Click opens modal */
marker.addListener("click", () => showCenterModal(center));
markers.push(marker);
});
map.fitBounds(bounds);
};
renderMarkers(centers);
/** Close modal on map click */
map.addListener("click", () => {
const modal = document.querySelector("[dev-target='map-modal']");
if (modal) modal.classList.remove("show");
});
const closeBtn = document.querySelector("[dev-target='map-modal-close']");
if (closeBtn) {
closeBtn.addEventListener("click", () => {
const modal = document.querySelector("[dev-target='map-modal']");
if (modal) modal.classList.remove("show");
});
}
/** ---------------------------------------------------
* Places Autocomplete (for dropdown search)
* --------------------------------------------------- */
const searchInput = document.querySelector("[dev-target='map-modal-search']");
if (!searchInput) {
console.error("Search input missing");
return;
}
const autocomplete = new google.maps.places.Autocomplete(searchInput, {
types: ["(regions)"],
fields: ["geometry", "name", "formatted_address"],
});
/** When user picks a place from dropdown */
autocomplete.addListener("gmpx-placechange", () => {
const place = autocomplete.getPlace();
if (!place.geometry) return;
const target = {
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
};
filterByProximity(target);
map.setCenter(target);
map.setZoom(6);
});
/**
* ---------------------------------------------------
* Search form submission handling
* --------------------------------------------------- */
// Create proper services
const autocompleteService = new google.maps.places.AutocompleteService();
const placesService = new google.maps.places.PlacesService(map);
const form = document.querySelector("[dev-target='search-form']");
if (!form) {
console.error("Element [dev-target='search-form'] not found");
return;
}
form.addEventListener("submit", (e) => {
e.preventDefault();
e.stopPropagation();
const query = searchInput.value.trim();
if (!query) {
console.error("Query is empty");
return;
};
autocompleteService.getPlacePredictions(
{ input: query },
(predictions, status) => {
if (
status !== google.maps.places.PlacesServiceStatus.OK ||
!predictions ||
predictions.length === 0
) return;
const first = predictions[0];
placesService.getDetails(
{
placeId: first.place_id,
fields: ["geometry", "name"]
},
(place, status2) => {
if (
status2 !== google.maps.places.PlacesServiceStatus.OK ||
!place?.geometry
) return;
const target = {
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng(),
};
filterByProximity(target);
map.setCenter(target);
map.setZoom(6);
}
);
}
);
});
/** ---------------------------------------------------
* Filter CMS centers by nearest to searched location
* --------------------------------------------------- */
function filterByProximity(target) {
const sorted = centers
.map(center => ({
...center,
distance: haversineDistance(
{ lat: center.latitude, lng: center.longitude },
target
)
}))
.sort((a, b) => a.distance - b.distance)
.slice(0, 10); // nearest 10 centers
renderMarkers(sorted);
}
}
window.initMap = initMap;
/** Inject Google Maps API */
const script = document.createElement("script");
script.src =
"https://maps.googleapis.com/maps/api/js?key=XXXXXXXXXX&callback=initMap&libraries=places,marker&v=weekly";
script.async = true;
document.head.appendChild(script);
});
In short:
Type something → we compare it with the address → show only the matching pins → update the map.
7. Final Result
You now have a:
Fully dynamic
CMS-powered
Custom-styled
Interactive Google Map
Searchable Google Map
Built completely inside Webflow
Every marker, location detail, and image comes directly from Webflow CMS. No code updates required.
Add a new location → republish site → new pin appears instantly.



