πͺ Custom Info Windows

What You'll Master in This Chapter
Create info windows that users actually want to interact with β
- Rich Interactive Experiences: Move beyond basic popups with HTML, CSS, and custom interactions
- Mobile-Responsive Design: CSS Grid, container queries, and layouts that adapt to any screen size
- Modern Styling Techniques: Dark mode support, animations, and premium visual effects
- Accessibility Features: Complete keyboard navigation and screen reader compatibility
- Performance Optimization: Lazy loading strategies and image optimization for fast loading
- Professional Architecture: From basic setup to advanced event handling and state management
The materials presented in this chapter deal with low-level APIs. If you're using a framework like React or Angular, make sure to check out the chapter on Working with Frameworks .
Map markers represent points of interest (POIs). But these POIs likely contain more information than just the geo position:
- If you're building a store locator, you'll want to display the stores' precise address, opening hours, and overall rating.
- If you're implementing real estate search, you'll want to preview the listing's photos, beds & baths, and price.
Since this extra information won't fit inside the marker label (pin), you have a few options:
- You can go with a modal but modals interrupt the user's workflow .
- You can present the information in a side panel (just like maps.google.com itself) but side panels consume valuable screen real estate . More importantly, side panels aren't SEO-friendly β unless clicking on the marker replaces the URL with the applicable href.
- Or, you can connect marker clicks to info windows (also called info bubbles or popups). This way, the information remains geographically contextualized and the user's workflow isn't interrupted. Naturally, you can link the info window content to a dedicated, SEO-friendly permalink.
I'm building a real estate search website. I already have a map containing custom markers. Now I need custom info windows to display the listing title, main photo, and price:

Screenshot courtesy of Garages-Near-Me.com
Also, I want my markers & info windows to be accessible to screen readers and keyboards:
2025 Update: This chapter now includes modern responsive design patterns, mobile-first layouts, CSS Grid/Flexbox implementations, and enhanced accessibility features for contemporary web applications.
Assuming the following listings data structure:
[
{
"title": "New underground parking: large, safe & perfectly located",
"price": "β¬140",
"photo": "https://...",
"address": "RienzistraΓe, 10318 Berlin",
"position": {
"lat": 52.4900596,
"lng": 13.5157737
}
},
{ ... }
]
you can easily implement dynamic SVG marker labels :
./svg.ts
export const generateSvgMarkup = (price: string) => ` <svg width="71" height="45" viewBox="0 0 71 45" fill="none" xmlns="http://www.w3.org/2000/svg"> <rect width="71" height="37" rx="10" fill="#010101" /> <path d="M35 45L27 37H43L35 45Z" fill="#010101" /> <text x="34" y="23" font-size="16px" font-family="system-ui" text-anchor="middle" fill="#FFFFFF" font-weight="bold"> ${price} </text> </svg>`; /** * Given a `rawSvgString` like `<svg>...</svg>`, return its URI-encoded representation. * @param {string} rawSvgString */ export function encodeSVG(rawSvgString: string): string { const symbols = /[\r\n%#()<>?\[\\\]^`{|}]/g; // Use single quotes instead of double to avoid URI encoding rawSvgString = rawSvgString .replace(/'/g, '"') .replace(/>\s+</g, "><") .replace(/\s{2,}/g, " "); return ( "data:image/svg+xml;utf-8," + rawSvgString.replace(symbols, encodeURIComponent) ); }
Now, as pointed out in π·οΈ Custom Marker Labels , markers can be made accessible through the title
property and by setting optimized
to false
.
This way, the click listener causes the marker to have button semantics, which can be accessed using keyboard navigation and via screen readers.
So, running:
import { listings } from "./listings";
import { encodeSVG, generateSvgMarkup } from "./svg";
for (const listing of listings) {
const marker = new google.maps.Marker({
map,
position: listing.position,
icon: {
url: encodeSVG(generateSvgMarkup(listing.price)),
scaledSize: new google.maps.Size(55, 35),
anchor: new google.maps.Point(27, 35),
},
title: listing.title,
optimized: false,
});
}
renders custom, yet focusable and clickable markers:
Once the clickable markers are ready, it's time to connect them to InfoWindow
s.
When constructing one, it's customary to provide:
- an
ariaLabel
describing its content - and the actual HTML
content
to render inside the info window wrapper.
The content
can be generated using modern HTML via template literals (This renders a ${var}
) or a templating engine like handlebars.js
:
const infowindow = new google.maps.InfoWindow({
ariaLabel: `Info window for ${listing.title}. Click to open its dedicated page.`,
content: `<div class="p-3 bg-white rounded-xl w-[220px]">
...
${listing.title}
...
${listing.price}
</div>`,
});
Since the HTML content
will be injected into the website's DOM, you can style it using CSS β unlike SVG markers which are already encoded and passed in as Data URLs .
In other words, you can either inject a <style>
tag as part of the content
:
const infowindow = new google.maps.InfoWindow({
...
content: `
<style>.infowindow-content { background: yellow; }</style>
<div class="infowindow-content">...</div>`
});
Or use targeted selectors in your site's CSS, e.g.:
#map .infowindow-content {
background: yellow;
}
In this chapter, we'll use Tailwind , the popular utilitarian CSS framework.
Once you've generated an InfoWindow
instance:
const infowindow = new google.maps.InfoWindow({
ariaLabel: `Info window for ${listing.title}. Click to open its dedicated page.`,
content: `
<div class="p-3 bg-white rounded-xl w-[220px]">
<article class="mb-5 last:mb-0 pb-5 smm:pb-6 smm:mb-6 last:mb-0 last:pb-0 mb-0 pb-0 smm:pb-0 smm:mb-0">
<a
href="https://garages-near-me.com"
rel="noreferrer"
target="_blank"
>
<div
class="w-full h-full flex smm:flex-col flex-col"
style="font-family: system-ui;"
>
<div class="h-32 overflow-hidden rounded-xl">
<div class="h-full relative overflow-hidden hover:transform hover:scale-110 transition-all duration-200">
<img
alt="${listing.title} - Photo 1 of 1"
src="${listing.photo}"
decoding="async"
class="rounded-xl bg-gray-200 transition-all duration-300 ease-linear"
sizes="(max-width: 525px) 414px, 256px" />
</div>
</div>
<div class="flex flex-col h-full w-full">
<header>
<span class="block font-semibold leading-tight whitespace-pre-wrap line-clamp-2 text-gray-600 mt-2 text-sm">${listing.title}</span>
</header>
<footer class="mt-auto justify-between text-sm">
<span class="block min-h-[1rem] mt-1 mb-0.5 text-sm text-gray-600">
<span class="inline-block">${listing.address}</span>
</span>
<span class="h-6 flex items-end text-gray-600 text-sm">
<span aria-label="${listing.price} per month">
<span class="font-bold tracking-tight">${listing.price}</span>
<span class="whitespace-pre-wrap inline-block">/ month</span>
</span>
</span>
</footer>
</div>
</div>
</a>
</article>
</div>`,
});
you can open it right away with:
infowindow.open({
// anchor it right above the marker
anchor: marker,
// connect it to the map
map,
});
If everything goes fine, you can see something along the lines of: