# Map (Caseroom component)

A location whose map loads from OpenStreetMap only when asked; the address works as a link without it. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

0.97% of the 2,062 sites in The UI Index have a map (20 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/map-embed.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/map-embed.json`
- HTML: https://www.themes.dev/caseroom/r/html/map-embed.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/components/map-embed/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### map-embed.astro

```astro
---
import { cn } from '@/lib/utils';
import Button from '../button/button.astro';
import { map as c, osmLink, PIN_PATH } from './map-embed.variants';

interface Props {
  name: string;
  address: string;
  lat: number;
  lon: number;
  class?: string;
}
const { name, address, lat, lon, class: className } = Astro.props;
---

<div data-map class={cn(c.root, className)}>
  <div data-map-frame class={c.frame}>
    <svg class={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" stroke-width="10" />
      <path d="M-10 105 L410 110 M135 -10 L140 260 M250 -10 L255 260 M355 -10 L350 260" stroke-width="4" />
    </svg>
    <span class={c.pin} aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d={PIN_PATH} /></svg></span>
    <Button size="sm" variant="outline" data-map-load="" data-lat={lat} data-lon={lon} data-title={`Map: ${name}`} class={c.load}>Show the map</Button>
  </div>
  <address class={c.info}>
    <span class={c.name}>{name}</span>
    <a href={osmLink(lat, lon)} class={cn(c.address, 'rounded-sm underline decoration-foreground/30 underline-offset-4 hover:decoration-foreground active:bg-accent')}>{address}</a>
    <span class={c.note}>The map loads from OpenStreetMap when you show it.</span>
  </address>
</div>

<script>
  import './map-embed.client';
</script>
```

### map-embed.client.ts

```tsx
// 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

```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

```tsx
/** 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}`;
```
