This example creates the traditional "bounce-drop" animation using CSS and
advanced markers. In the IntersectionObserver
, it adds the
drop
CSS style. The IntersectionObserver
sees when each marker enters the
viewport, and adds the style. Then, the animationend
event listener that the
createMarker()
function added to each marker removes the style.
Read the documentation.
/** * Returns a random lat lng position within the map bounds. * @param {!google.maps.Map} map * @return {!google.maps.LatLngLiteral} */ function getRandomPosition(map) { const bounds = map.getBounds(); const minLat = bounds.getSouthWest().lat(); const minLng = bounds.getSouthWest().lng(); const maxLat = bounds.getNorthEast().lat(); const maxLng = bounds.getNorthEast().lng(); const latRange = maxLat - minLat; // Note: longitude can span from a positive longitude in the west to a // negative one in the east. e.g. 150lng (150E) <-> -30lng (30W) is a large // span that covers the whole USA. let lngRange = maxLng - minLng; if (maxLng < minLng) { lngRange += 360; } return { lat: minLat + Math.random() * latRange, lng: minLng + Math.random() * lngRange, }; } const total = 100; const intersectionObserver = new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting) { entry.target.classList.add('drop'); intersectionObserver.unobserve(entry.target); } } }); async function initMap(): Promise<void> { // Request needed libraries. const { Map } = await google.maps.importLibrary("maps") as google.maps.MapsLibrary; const { AdvancedMarkerElement, PinElement } = await google.maps.importLibrary("marker") as google.maps.MarkerLibrary; const position = {lat: 37.4242011827985, lng: -122.09242296450893}; const map = new Map(document.getElementById("map") as HTMLElement, { zoom: 14, center: position, mapId: '4504f8b37365c3d0', }); // Create 100 markers to animate. google.maps.event.addListenerOnce(map, 'idle', () => { for (let i = 0; i < 100; i++) { createMarker(map, AdvancedMarkerElement, PinElement); } }); // Add a button to reset the example. const controlDiv = document.createElement("div"); const controlUI = document.createElement("button"); controlUI.classList.add("ui-button"); controlUI.innerText = "Reset the example"; controlUI.addEventListener("click", () => { // Reset the example by reloading the map iframe. refreshMap(); }); controlDiv.appendChild(controlUI); map.controls[google.maps.ControlPosition.TOP_CENTER].push(controlDiv); } function createMarker(map, AdvancedMarkerElement, PinElement) { const pinElement = new PinElement(); const content = pinElement.element; const advancedMarker = new AdvancedMarkerElement({ position: getRandomPosition(map), map: map, content: content, }); content.style.opacity = '0'; content.addEventListener('animationend', (event) => { content.classList.remove('drop'); content.style.opacity = '1'; }); const time = 2 + Math.random(); // 2s delay for easy to see the animation content.style.setProperty('--delay-time', time +'s'); intersectionObserver.observe(content); } function refreshMap() { // Refresh the map. const mapContainer = document.getElementById('mapContainer'); const map = document.getElementById('map'); map!.remove(); const mapDiv = document.createElement('div'); mapDiv.id = 'map'; mapContainer!.appendChild(mapDiv); initMap(); } initMap();
/** * Returns a random lat lng position within the map bounds. * @param {!google.maps.Map} map * @return {!google.maps.LatLngLiteral} */ function getRandomPosition(map) { const bounds = map.getBounds(); const minLat = bounds.getSouthWest().lat(); const minLng = bounds.getSouthWest().lng(); const maxLat = bounds.getNorthEast().lat(); const maxLng = bounds.getNorthEast().lng(); const latRange = maxLat - minLat; // Note: longitude can span from a positive longitude in the west to a // negative one in the east. e.g. 150lng (150E) <-> -30lng (30W) is a large // span that covers the whole USA. let lngRange = maxLng - minLng; if (maxLng < minLng) { lngRange += 360; } return { lat: minLat + Math.random() * latRange, lng: minLng + Math.random() * lngRange, }; } const total = 100; const intersectionObserver = new IntersectionObserver((entries) => { for (const entry of entries) { if (entry.isIntersecting) { entry.target.classList.add('drop'); intersectionObserver.unobserve(entry.target); } } }); async function initMap() { // Request needed libraries. const { Map } = await google.maps.importLibrary("maps"); const { AdvancedMarkerElement, PinElement } = await google.maps.importLibrary("marker"); const position = { lat: 37.4242011827985, lng: -122.09242296450893 }; const map = new Map(document.getElementById("map"), { zoom: 14, center: position, mapId: '4504f8b37365c3d0', }); // Create 100 markers to animate. google.maps.event.addListenerOnce(map, 'idle', () => { for (let i = 0; i < 100; i++) { createMarker(map, AdvancedMarkerElement, PinElement); } }); // Add a button to reset the example. const controlDiv = document.createElement("div"); const controlUI = document.createElement("button"); controlUI.classList.add("ui-button"); controlUI.innerText = "Reset the example"; controlUI.addEventListener("click", () => { // Reset the example by reloading the map iframe. refreshMap(); }); controlDiv.appendChild(controlUI); map.controls[google.maps.ControlPosition.TOP_CENTER].push(controlDiv); } function createMarker(map, AdvancedMarkerElement, PinElement) { const pinElement = new PinElement(); const content = pinElement.element; const advancedMarker = new AdvancedMarkerElement({ position: getRandomPosition(map), map: map, content: content, }); content.style.opacity = '0'; content.addEventListener('animationend', (event) => { content.classList.remove('drop'); content.style.opacity = '1'; }); const time = 2 + Math.random(); // 2s delay for easy to see the animation content.style.setProperty('--delay-time', time + 's'); intersectionObserver.observe(content); } function refreshMap() { // Refresh the map. const mapContainer = document.getElementById('mapContainer'); const map = document.getElementById('map'); map.remove(); const mapDiv = document.createElement('div'); mapDiv.id = 'map'; mapContainer.appendChild(mapDiv); initMap(); } initMap(); export {};
/* * Always set the map height explicitly to define the size of the div element * that contains the map. */ #map { height: 100%; } /* * Optional: Makes the sample page fill the window. */ html, body { height: 100%; margin: 0; padding: 0; } /* set the default transition time */ :root { --delay-time: .5s; } #map { height: 100%; } #mapContainer { height: 100%; } html, body { height: 100%; margin: 0; padding: 0; } @keyframes drop { 0% { transform: translateY(-200px) scaleY(0.9); opacity: 0; } 5% { opacity: 0.7; } 50% { transform: translateY(0px) scaleY(1); opacity: 1; } 65% { transform: translateY(-17px) scaleY(0.9); opacity: 1; } 75% { transform: translateY(-22px) scaleY(0.9); opacity: 1; } 100% { transform: translateY(0px) scaleY(1); opacity: 1; } } .drop { animation: drop 0.3s linear forwards var(--delay-time); } .ui-button { background-color: #fff; border: 0; border-radius: 2px; box-shadow: 0 1px 4px -1px rgba(0, 0, 0, 0.3); margin: 10px; padding: 0 0.5em; font: 400 18px Roboto, Arial, sans-serif; overflow: hidden; height: 40px; cursor: pointer; } .ui-button:hover { background: rgb(235, 235, 235); }
<html> <head> <title>Advanced Markers CSS Animation</title> <link rel="stylesheet" type="text/css" href="./style.css" /> <script type="module" src="./index.js"></script> </head> <body> <div id="mapContainer"> <div id="map" style="height: 100%"></div> </div> <!-- prettier-ignore --> <script>(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))}) ({key: "AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8", v: "weekly"});</script> </body> </html>
Clone Sample
Git and Node.js are required to run this sample locally. Follow these instructions to install Node.js and NPM. The following commands clone, install dependencies and start the sample application.
git clone https://github.com/googlemaps-samples/js-api-samples.git
cd samples/advanced-markers-animation
npm i
npm start