πŸͺŸ Custom Info Windows

Spatialized founder Jozef Sorocin
Book a consultation β†—
17 min read  β€’  Updated 09/15/2025

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:

  1. You can go with a modal but modals interrupt the user's workflow .
  2. 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.
  3. 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

Screenshot courtesy of Garages-Near-Me.com

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:

Accessible marker labels
Accessible marker labels
Accessible marker labels

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:

Semi-styled info window
Semi-styled info window
Semi-styled info window

Join 200+ developers who've mastered this! Get Complete Access β€” €19
Already a member? Sign in here