OnlyMapJS Latest release
GitHub Docs

← Gallery / Widgets

Use the responsive widget manager

Place widgets in eight logical slots that stack, cluster, and fold into drawers on narrow screens.

Open full screen ↗ View source ↗
<!DOCTYPE html>
<!--
  Widgets that get out of the way. Under a width breakpoint every managed
  widget folds into a drawer on its own side of the map — a dashboard that
  is generous on a desktop stays usable in a phone-width embed, with
  nothing to author per widget. `fold="never"` opts one widget out, and a
  single attribute hides every widget at once.

  The map: San Francisco with widgets on all four sides. Drag the width
  slider in the bar above and watch them fold into drawers at the
  breakpoint; edit the breakpoint, then toggle auto-fold, hide-all and RTL
  to try each rule on its own.
-->
<html>
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>OnlyMapJS — Use the responsive widget manager</title>
  <link rel="stylesheet" href="https://unpkg.com/@nika-js/onlymap/dist/onlymapjs.css" />
  <script type="module" src="https://unpkg.com/@nika-js/onlymap/dist/onlymap.standalone.js"></script>
  <style>
    html, body { margin: 0; height: 100%; font-family: system-ui, sans-serif; }
    om-map { display: block; height: 100vh; }
  </style>
  <style>
    body { overflow: hidden; background: #e8edf4; }
    .lab {
      width: 100%;
      min-width: 0;
      height: calc(100vh - 44px);
      min-height: 560px;
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      grid-template-rows: auto minmax(0, 1fr);
    }
    .lab-controls {
      box-sizing: border-box;
      min-width: 0;
      z-index: 2;
      display: flex;
      align-items: center;
      flex-wrap: wrap;
      gap: 10px 18px;
      padding: 10px 16px;
      color: #e5eefb;
      background: #142033;
      box-shadow: 0 2px 10px rgba(15, 23, 42, .22);
      font-size: 12px;
    }
    .lab-controls strong { color: #fff; }
    .lab-controls label { display: inline-flex; align-items: center; gap: 7px; }
    .lab-controls input[type="range"] { width: 150px; accent-color: #60a5fa; }
    .lab-controls input[type="number"] {
      width: 58px;
      padding: 4px 6px;
      border: 1px solid #64748b;
      border-radius: 5px;
      color: #f8fafc;
      background: #25344d;
    }
    .lab-controls output {
      min-width: 44px;
      color: #93c5fd;
      font-variant-numeric: tabular-nums;
    }
    .map-stage {
      min-width: 0;
      min-height: 0;
      display: grid;
      place-items: center;
      padding: 14px;
    }
    .map-frame {
      box-sizing: border-box;
      width: min(1180px, 100%);
      height: 100%;
      min-height: 430px;
      overflow: hidden;
      border: 1px solid #94a3b8;
      border-radius: 12px;
      background: #dbeafe;
      box-shadow: 0 10px 30px rgba(15, 23, 42, .2);
    }
    #widget-manager-demo {
      width: 100%;
      height: 100%;
      --om-widget-fold-breakpoint: 720px;
      --om-widget-gap-x: 10px;
      --om-widget-gap-y: 10px;
    }
    @media (max-width: 600px) {
      .lab-controls { gap: 8px 12px; }
      .lab-controls input[type="range"] { width: 100px; }
      .map-stage { padding: 8px; }
    }
  </style>
</head>
<body>

  <main class="lab">
    <form class="lab-controls">
      <strong>v0.4.4 test bench</strong>
      <label>
        Map width
        <input id="map-width" type="range" min="320" max="1180" value="1180" />
        <output id="map-width-value" for="map-width">auto</output>
      </label>
      <label>
        Breakpoint
        <input id="fold-breakpoint" type="number" min="320" max="1200" step="10" value="720" />
        px
      </label>
      <label><input id="auto-fold" type="checkbox" checked /> Auto-fold</label>
      <label><input id="hide-widgets" type="checkbox" /> Hide widgets</label>
      <label><input id="rtl-layout" type="checkbox" /> RTL layout</label>
    </form>

    <section class="map-stage">
      <div class="map-frame" id="map-frame">
        <om-map id="widget-manager-demo" center="[-122.42, 37.77]" zoom="12" basemap="dark-matter">
          <om-layer id="regions" type="GeoJsonLayer"
                    data="../../data/sf-districts.geojson"
                    label="SF districts"
                    color="#facc15"
                    filled stroked pickable
                    get-fill-color="[250, 204, 21, 215]"
                    get-line-color="[254, 240, 138]"
                    line-width-min-pixels="2"></om-layer>

          <om-layer id="bikes" type="ScatterplotLayer"
                    data="../../data/bikes.json"
                    label="Bike racks"
                    color="#ef4444"
                    get-position="[$lon, $lat]"
                    get-radius="scale($size, sqrt, [4, 18], domain=[0, 100])"
                    radius-units="pixels"></om-layer>

          <!-- This compact title remains on the map while every other
               managed widget folds into a side drawer. -->
          <om-widget id="persistent-title" position="top-start" fold="never">
            <style>
              .title {
                box-sizing: border-box;
                width: 132px;
                padding: 8px 10px;
                border: 1px solid rgba(59,130,246,.3);
                border-radius: 8px;
                color: #0f172a;
                background: rgba(255,255,255,.94);
                box-shadow: 0 3px 12px rgba(15,23,42,.16);
                font: 11px/1.3 system-ui, sans-serif;
              }
              strong { display: block; font-size: 12px; color: #1d4ed8; }
            </style>
            <div class="title">
              <strong>Widget Manager</strong>
              v0.4.4 · persistent
            </div>
          </om-widget>

          <!-- Top-side drawer. -->
          <om-widget type="legend" title="Top drawer · Layers"
                     interactive position="top-end"></om-widget>

          <!-- Logical start-side drawer. -->
          <om-widget type="layer-switcher" title="Start drawer"
                     position="center-start"></om-widget>

          <!-- Logical end-side drawer. Opening a drawer moves focus to its
               first control — the button here is where it lands. -->
          <om-widget id="focus-demo" position="center-end">
            <style>
              .focus-card {
                width: 174px;
                padding: 11px;
                border-radius: 8px;
                background: #fff;
                color: #1f2937;
                box-shadow: 0 3px 14px rgba(0,0,0,.18);
                font: 12px/1.35 system-ui, sans-serif;
              }
              button {
                width: 100%;
                margin-top: 8px;
                padding: 7px;
                border: 0;
                border-radius: 6px;
                background: #2563eb;
                color: #fff;
                cursor: pointer;
              }
              button:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; }
            </style>
            <div class="focus-card">
              <strong>End drawer</strong><br />
              Opening a drawer moves focus to its first control.
              <button type="button">Focus lands here</button>
            </div>
          </om-widget>

          <!-- Bottom-side drawer. Compact controls stay clustered as they
               move into and back out of it. -->
          <om-widget type="zoom-controls" position="bottom-center"></om-widget>
          <om-widget type="undo-redo" position="bottom-center"></om-widget>
          <om-widget type="scale-bar" position="bottom-center"></om-widget>

          <!-- The toggle never folds or hides — it has to stay reachable
               to reverse hide-all. -->
          <om-widget type="widgets-toggle" position="bottom-end"></om-widget>

          <om-fallback>
            <p><strong>This responsive widget demo requires JavaScript.</strong></p>
          </om-fallback>
        </om-map>
      </div>
    </section>
  </main>

  <script type="module">
    const map = document.querySelector("#widget-manager-demo");
    const frame = document.querySelector("#map-frame");
    const width = document.querySelector("#map-width");
    const widthValue = document.querySelector("#map-width-value");
    const breakpoint = document.querySelector("#fold-breakpoint");
    const autoFold = document.querySelector("#auto-fold");
    const hideWidgets = document.querySelector("#hide-widgets");
    const rtl = document.querySelector("#rtl-layout");
    let requestedWidth = Number(width.value);

    document.querySelector(".lab-controls").addEventListener("submit", (event) => event.preventDefault());

    const fitWidthControl = () => {
      const available = Math.max(320, document.querySelector(".map-stage").clientWidth - 28);
      width.max = String(Math.min(1180, available));
      width.value = String(Math.min(requestedWidth, Number(width.max)));
      frame.style.width = `${width.value}px`;
      widthValue.value = `${Math.round(frame.getBoundingClientRect().width)}px`;
    };

    width.addEventListener("input", () => {
      requestedWidth = Number(width.value);
      fitWidthControl();
    });
    window.addEventListener("resize", fitWidthControl);
    breakpoint.addEventListener("input", () => {
      map.style.setProperty("--om-widget-fold-breakpoint", `${breakpoint.value}px`);
      // ResizeObserver is width-driven; nudge the frame so a live breakpoint
      // edit is observed without requiring a browser resize.
      frame.style.width = `calc(${width.value}px - 1px)`;
      requestAnimationFrame(() => { frame.style.width = `${width.value}px`; });
    });
    autoFold.addEventListener("change", () => {
      if (autoFold.checked) map.removeAttribute("widgets-fold");
      else map.setAttribute("widgets-fold", "off");
    });
    hideWidgets.addEventListener("change", () => {
      map.toggleAttribute("widgets-hidden", hideWidgets.checked);
    });
    rtl.addEventListener("change", () => {
      map.dir = rtl.checked ? "rtl" : "ltr";
    });

    fitWidthControl();
  </script>
</body>
</html>

The snippet above loads the latest release. The demo above it is pinned to v0.5.2, so it keeps working when a new version ships.