Local & Weather

Free Map Embed Card Widget

Embed an interactive OpenStreetMap location card with a marker, no API key needed.

A lightweight map card that drops an interactive OpenStreetMap embed with a pin anywhere on your page, with an optional title and caption. Built for local businesses, restaurants, and event sites that want a "find us" map without Google Maps API keys or billing setup. Install by pasting a single script tag with your latitude and longitude where you want the map to appear.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/map-embed-card.js" data-hl-lat="35.2271" data-hl-lon="-80.8431" data-hl-height="340" data-hl-title="Visit us in Uptown Charlotte" data-hl-caption="Tryon Street, Charlotte, NC — street parking and the 7th St light rail stop are a block away." async></script>
<!-- Free Map Embed Card widget by haynes lab — https://hayneslab.dev/tools/widgets/map-embed-card -->
Inline (self-contained)html
<!-- Free Map Embed Card widget by haynes lab — https://hayneslab.dev/tools/widgets/map-embed-card -->
<script data-hl-lat="35.2271" data-hl-lon="-80.8431" data-hl-height="340" data-hl-title="Visit us in Uptown Charlotte" data-hl-caption="Tryon Street, Charlotte, NC — street parking and the 7th St light rail stop are a block away.">
/*!
 * haynes lab — Map Embed Card widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/map-embed-card
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-map-embed-card';
  var STYLE_ID = 'hlw-map-embed-card-styles';

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/map-embed-card.js"]');
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 720px; margin: 0 auto; overflow: hidden;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 16px; line-height: 1.5; color: #1f2937; background: #ffffff;' +
      ' border: 1px solid #e5e7eb; border-radius: 12px; }' +
      '.' + PREFIX + '__title { padding: 16px 20px 12px; font-weight: 600; font-size: 17px;' +
      ' color: #111827; border-bottom: 1px solid #f3f4f6; }' +
      '.' + PREFIX + '__frame { display: block; width: 100%; border: 0; }' +
      '.' + PREFIX + '__caption { padding: 12px 20px 4px; font-size: 14px; color: #6b7280; }' +
      '.' + PREFIX + '__footer { display: flex; align-items: center; justify-content: space-between;' +
      ' gap: 12px; padding: 10px 14px 12px; }' +
      '.' + PREFIX + '__link { font-size: 13px; color: #4b5563; text-decoration: none; }' +
      '.' + PREFIX + '__link:hover { color: #111827; text-decoration: underline; }' +
      '.' + PREFIX + '__link:focus-visible { outline: 2px solid #f97316; outline-offset: 2px; }' +
      '.' + PREFIX + '__credit { font-size: 11px; display: inline-flex; align-items: center; gap: 4px;' +
      ' color: #9ca3af; text-decoration: none; white-space: nowrap; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }' +
      '@media (max-width: 560px) { .' + PREFIX + '__footer { flex-direction: column; align-items: flex-start; gap: 6px; } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function clamp(n, min, max) {
    return Math.min(max, Math.max(min, n));
  }

  function build(script) {
    var lat = parseFloat(script.getAttribute('data-hl-lat'));
    var lon = parseFloat(script.getAttribute('data-hl-lon'));
    if (isNaN(lat) || isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
      console.warn('[haynes lab map-embed-card] data-hl-lat and data-hl-lon are required and must be valid coordinates. Nothing rendered.');
      return;
    }

    var zoom = parseInt(script.getAttribute('data-hl-zoom'), 10);
    if (isNaN(zoom)) zoom = 14;
    zoom = clamp(zoom, 2, 19);

    var height = parseInt(script.getAttribute('data-hl-height'), 10);
    if (isNaN(height)) height = 360;
    height = clamp(height, 200, 1200);

    var title = script.getAttribute('data-hl-title');
    var caption = script.getAttribute('data-hl-caption');

    // Approximate the visible lat/lon span for a web-mercator map at this zoom.
    var scale = 360 / (256 * Math.pow(2, zoom)); // degrees per pixel
    var deltaLon = scale * 640;
    var deltaLat = scale * height * Math.cos(lat * Math.PI / 180);
    var bbox = [
      clamp(lon - deltaLon / 2, -180, 180),
      clamp(lat - deltaLat / 2, -85, 85),
      clamp(lon + deltaLon / 2, -180, 180),
      clamp(lat + deltaLat / 2, -85, 85),
    ].map(function (n) { return n.toFixed(6); }).join(',');

    injectStyles();

    var root = el('div', PREFIX);

    if (title) root.appendChild(el('div', PREFIX + '__title', title));

    var frame = el('iframe', PREFIX + '__frame');
    frame.src = 'https://www.openstreetmap.org/export/embed.html?bbox=' +
      encodeURIComponent(bbox) + '&layer=mapnik&marker=' +
      encodeURIComponent(lat + ',' + lon);
    frame.style.height = height + 'px';
    frame.setAttribute('loading', 'lazy');
    frame.setAttribute('title', title ? 'Map of ' + title : 'Map');
    root.appendChild(frame);

    if (caption) root.appendChild(el('p', PREFIX + '__caption', caption));

    var footer = el('div', PREFIX + '__footer');

    var link = el('a', PREFIX + '__link', 'View larger map');
    link.href = 'https://www.openstreetmap.org/?mlat=' + encodeURIComponent(lat) +
      '&mlon=' + encodeURIComponent(lon) + '#map=' + zoom + '/' + lat + '/' + lon;
    link.target = '_blank';
    link.rel = 'noopener';
    footer.appendChild(link);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/map-embed-card';
    credit.target = '_blank';
    credit.rel = 'noopener';
    footer.appendChild(credit);

    root.appendChild(footer);

    if (script.parentNode && script.parentNode.nodeName !== 'HEAD') {
      script.insertAdjacentElement('afterend', root);
    } else {
      document.body.appendChild(root);
    }
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab map-embed-card] Could not locate its own script tag. Nothing rendered.');
      return;
    }
    build(script);
  }

  if (document.body) {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-lat(required)Latitude of the location the map centers on and marks (e.g. "35.2271").
data-hl-lon(required)Longitude of the location the map centers on and marks (e.g. "-80.8431").
data-hl-zoom"14"Map zoom level from 2 (world) to 19 (street detail).
data-hl-height"360"Height of the map frame in pixels (200–1200); width always fills its container.
data-hl-title(none)Optional heading shown above the map.
data-hl-caption(none)Optional caption text shown below the map, e.g. an address or directions note.

Frequently asked questions

How do I install the map embed card?

Paste the script tag with your data-hl-lat and data-hl-lon where you want the map to appear — it renders inline right after the tag.

Can I change the zoom, size, or text?

Yes — use data-hl-zoom (2–19) for the zoom level, data-hl-height for the frame height in pixels, and data-hl-title / data-hl-caption for the text above and below the map.

Does it need a Google Maps API key?

No — it embeds OpenStreetMap, which is free and requires no API key, account, or billing setup.

Does it work on WordPress, Shopify, or Squarespace?

Yes — it's a single vanilla JS file with no dependencies, so it works anywhere you can paste a script tag, including WordPress, Shopify, Squarespace, and plain HTML.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch