← Gallery / Widgets
Use the responsive widget manager
Place widgets in eight logical slots that stack, cluster, and fold into drawers on narrow screens.
<!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.