Skip to main content

Command Palette

Search for a command to run...

How to Build Dynamic, CMS-Powered Google Maps in Webflow

CMS Powered maps in webflow With Custom Styling and location filtering

Updated
11 min read
How to Build Dynamic, CMS-Powered Google Maps in Webflow
B

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 NamePurposeExample
NameMarker title“Valley Forge Medical Center”
LatitudeGoogle Maps latitude40
LongitudeGoogle Maps longitude-75
AddressFull location address“657 Valley Forge Rd…”
PhoneContact phone number(610) 555-0349
FaxOptional extra field(610) 555-0350
ImagePhoto displayed in the modalJPG/PNG
Image URL (optional)Direct asset URLWebflow 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_HERE

  • YOUR_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

  1. Log into Google Cloud Console.

  2. Go to Maps Management → Map IDs.

  3. Click Create Map ID.

  4. Enter name + description.

  5. Choose Platform: JavaScript.

  6. Choose Map Type: Map.

  7. Click Save.

You’ll get something like:

0123abcd4567ef89

Step 2 — Create & Associate a Map Style

  1. In Maps Management, click your new Map ID.

  2. Scroll to Map styles.

  3. Choose a mode (Light / Dark / Auto).

  4. Click Edit.

  5. 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
              }
            }
          ]
        }
      
  6. 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.