Components · Media
Map
A location whose map loads from OpenStreetMap only when asked; the address works as a link without it.
0.97% of the 2,062 sites in The UI Index have a map (20 sites).
Northwind, Utrecht officeOudegracht 120, 3511 AW UtrechtThe map loads from OpenStreetMap when you show it.
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/ caseroom/ r/ map-embed.json - Astro
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ astro/ map-embed.json
Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS and the script.
// 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 {};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>
);
}/** 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}`;---
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: "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 {};/** 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}`;<div data-map class="grid w-full overflow-hidden rounded-xl border @container max-w-md">
<div data-map-frame class="relative aspect-[16/10] bg-muted">
<svg class="absolute inset-0 size-full text-border" 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><path d="M-10 105 L410 110 M135 -10 L140 260 M250 -10 L255 260 M355 -10 L350 260" stroke-width="4"></path></svg>
<span class="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" 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="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"></path></svg>
</span>
<button type="button" class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-pressed h-8 px-3 text-[13px] absolute right-3 bottom-3" data-map-load data-lat="52.0894" data-lon="5.1195" data-title="Map: Northwind, Utrecht office">
Show the map
</button>
</div>
<address class="grid gap-1 border-t px-4 py-3 text-sm">
<span class="font-medium">Northwind, Utrecht office</span>
<a href="https://www.openstreetmap.org/?mlat=52.0894&mlon=5.1195#map=17/52.0894/5.1195" class="w-fit text-muted-foreground not-italic rounded-sm underline decoration-foreground/30 underline-offset-4 hover:decoration-foreground active:bg-accent">
Oudegracht 120, 3511 AW Utrecht
</a>
<span class="text-xs text-muted-foreground">The map loads from OpenStreetMap when you show it.</span>
</address>
</div><script type="module" src="/caseroom/r/html/map-embed.js"></script>