Skip to content
    Style: Wireframe

    Components · Media

    Map

    A location whose map loads from OpenStreetMap only when asked; the address works as a link without it.

    1.0% of the 2,073 sites in The UI Index have a map (21 sites).

    Northwind, Utrecht officeOudegracht 120, 3511 AW UtrechtThe map loads from OpenStreetMap when you show it.

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/map-embed.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/map-embed.json

    Or copy the files below into your project. They need clsx, tailwind-merge, and the themes.dev UI theme. The HTML needs only the compiled CSS and the script.

    map-embed.client.ts
    // Map embed: "Show the map" puts the provider's iframe in place of the drawn pattern (the first request to the
    // provider happens here) and moves focus to it. The URL is built here from numeric data-lat / data-lon, never taken
    // from the markup, so the handler can't be used to load another address; the iframe is sandboxed and sends no
    // referrer.
    import { map, osmEmbed } from './map-embed.variants';
    
    declare global {
      interface Window {
        __caseroomMap?: boolean;
      }
    }
    
    if (typeof window !== 'undefined' && !window.__caseroomMap) {
      window.__caseroomMap = true;
      document.addEventListener('click', (e) => {
        const b = (e.target as Element).closest<HTMLButtonElement>('[data-map-load]');
        if (!b) return;
        const frame = b.closest('[data-map]')?.querySelector('[data-map-frame]');
        if (!frame) return;
        const lat = Number(b.dataset.lat);
        const lon = Number(b.dataset.lon);
        if (!Number.isFinite(lat) || !Number.isFinite(lon) || Math.abs(lat) > 90 || Math.abs(lon) > 180) return;
        const iframe = document.createElement('iframe');
        iframe.src = osmEmbed(lat, lon);
        iframe.title = b.dataset.title ?? 'Map';
        iframe.loading = 'lazy';
        iframe.referrerPolicy = 'no-referrer';
        iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
        iframe.className = map.iframe;
        frame.replaceChildren(iframe);
        iframe.focus();
      });
    }
    
    export {};
    map-embed.tsx
    import { cn } from '@/lib/utils';
    import { Button } from '../button/button';
    import './map-embed.client';
    import { map as c, osmLink, PIN_PATH } from './map-embed.variants';
    
    export interface MapEmbedProps {
      name: string;
      address: string;
      lat: number;
      lon: number;
      className?: string;
    }
    
    /** A location whose map loads from OpenStreetMap only when asked. */
    export function MapEmbed({ name, address, lat, lon, className }: MapEmbedProps) {
      return (
        <div data-map="" className={cn(c.root, className)}>
          <div data-map-frame="" className={c.frame}>
            <svg className={c.pattern} viewBox="0 0 400 250" preserveAspectRatio="xMidYMid slice" fill="none" stroke="currentColor" aria-hidden="true">
              <path d="M-10 60 L410 40 M-10 150 L410 175 M-10 220 L410 205 M80 -10 L60 260 M190 -10 L215 260 M310 -10 L290 260" strokeWidth="10" />
              <path d="M-10 105 L410 110 M135 -10 L140 260 M250 -10 L255 260 M355 -10 L350 260" strokeWidth="4" />
            </svg>
            <span className={c.pin} aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d={PIN_PATH} /></svg></span>
            <Button size="sm" variant="outline" {...{ 'data-map-load': '', 'data-lat': String(lat), 'data-lon': String(lon), 'data-title': `Map: ${name}` }} className={c.load}>Show the map</Button>
          </div>
          <address className={c.info}>
            <span className={c.name}>{name}</span>
            <a href={osmLink(lat, lon)} className={cn(c.address, 'rounded-sm underline decoration-foreground/30 underline-offset-4 hover:decoration-foreground active:bg-accent')}>{address}</a>
            <span className={c.note}>The map loads from OpenStreetMap when you show it.</span>
          </address>
        </div>
      );
    }
    map-embed.variants.ts
    /** A location with a map that loads only when asked (no third-party request before the click): the address and
     *  a drawn street pattern with a pin, then "Show the map" swaps in the map provider's embed. The address stays a
     *  link to the provider, so it works without the embed too. */
    export const map = {
      root: 'grid w-full overflow-hidden rounded-xl border @container',
      frame: 'relative aspect-[16/10] bg-muted',
      pattern: 'absolute inset-0 size-full text-border',
      pin: 'absolute top-1/2 left-1/2 grid size-10 -translate-x-1/2 -translate-y-full place-items-center rounded-full bg-primary text-primary-foreground shadow-lg [&_svg]:size-5',
      load: 'absolute right-3 bottom-3',
      iframe: 'absolute inset-0 size-full border-0',
      info: 'grid gap-1 border-t px-4 py-3 text-sm',
      name: 'font-medium',
      address: 'w-fit text-muted-foreground not-italic',
      note: 'text-xs text-muted-foreground',
    };
    export const PIN_PATH = 'M8 14.5s4.5-4.2 4.5-8a4.5 4.5 0 0 0-9 0c0 3.8 4.5 8 4.5 8zM8 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z';
    /** OpenStreetMap's embed around a point (no key needed). */
    export const osmEmbed = (lat: number, lon: number, d = 0.006) => `https://www.openstreetmap.org/export/embed.html?bbox=${lon - d}%2C${lat - d / 2}%2C${lon + d}%2C${lat + d / 2}&layer=mapnik&marker=${lat}%2C${lon}`;
    export const osmLink = (lat: number, lon: number) => `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=17/${lat}/${lon}`;