Overview
OnlyMap JS (@nika-js/onlymap) builds an interactive WebGL map from a small set of HTML custom elements. You write the layers, widgets, popups, interactions, and guided tours as markup. The library does the rendering, the data loading, the live updates, the picking, and the validation. You do not write deck.gl setup code, shader code, or a build step. One HTML file is a complete map application.
OnlyMap JS is also made for AI agents to write. HTML is a reliable output format for a model. The package includes an agent skill and an llms.txt file. OmMap.validate() returns structured errors, and each error has a fix. Together these give an agent a real authoring loop.
Install
Install the package from npm:@nika-js/onlymap@0.6.19 or later. Use a raw-file CDN (unpkg, jsDelivr) — the bare package URL serves a single-file standalone bundle built for this. Do not use a rebundling CDN such as esm.sh or skypack: re-bundling duplicates the deck.gl/luma.gl runtime and every layer fails shader compilation.
Building a mobile app? The same runtime ships as an Expo React Native
adapter, @nika-js/onlymap-native — see React Native.
Two optional commands complete the setup:
npx @nika-js/onlymap init— adds VS Code IntelliSense for everyom-*element and attribute. It also adds!-prefix snippets (!starter,!map, …).npx -y skills add NikaGeospatial/onlymapjs --skill onlymapjs— installs the OnlyMap authoring skill into coding agents (Claude Code, Cursor, Codex). The agents then write correct manifests on the first try.
Key Features
Declarative Everything
- 41 layer types by name: all of deck.gl’s core, geo, aggregation, and mesh layers (Scatterplot, GeoJson, Arc, Path, Heatmap, Hexagon, Trips, Tile, Tile3D, Scenegraph, and more). It adds a built-in
PopupLayerfor WebGL labels at scale, a nativeCOGLayerfor GeoTIFF rasters, aZarrLayerfor chunked Zarr/GeoZarr rasters, anImageOverlayfor georeferenced drone photos, aBIMLayerfor in-browser IFC models,Route/Trackingfor styled routes and live position tracking, aHillshadeLayerfor shaded relief, and aTextOnPathLayerfor labels that follow a line. - Live manifest: the markup is the state. When you change an attribute — from a widget, a behavior, a script, or devtools — the map updates automatically.
- Satellite imagery without a key new in 0.11.10:
basemap="satellite"shows a cloud-free satellite view of the whole world (EOX Sentinel-2 cloudless), free for commercial use with its credit — detailed to about city-district zoom. Sharper, current imagery remains the keyedmaptiler-satellite. - Built-in undo and redo: you can undo a layer toggle, a filter change, a basemap switch, an element edit, or a drawn sketch. Use
<om-widget type="undo-redo">or Cmd/Ctrl-Z. - Clustering as a toggle new in 0.11.0:
cluster="true"on a point layer merges the points that would overlap into count bubbles that split apart as the reader zooms in — same layer, same data, same styling for the points that stand alone.cluster-propertiescarries sums, means or the commonest category onto each bubble, bubble sizes stay meaningful at every zoom, and a click flies to where a bubble splits. See Clustering. - Print-ready cartographs new in 0.8.0: the separate
@nika-js/onlymap/cartographentry composes millimetre-based map sheets with live or static georeferenced frames, derived legends, graticules, scale bars, north arrows, press marks, and atlas export. It stays out of the interactive-map bundle, and a static-only sheet never loads the map runtime. A frame can name fallback sources 0.10.6 — a capture it shows when its live map cannot load where the sheet is opened — and an atlas can be paged by its reader 0.10.9 with the keyboard or an on-sheet control. See Cartographs & Atlases. - Maps in a real projection new in 0.10.0:
crs="EPSG:5070"on<om-map>draws the whole map in that projection’s plane — parallels curve, an equal-area map is genuinely equal-area, a national-grid sheet is drawn in its own grid. Albers, the UTM zones, British National Grid, Swiss LV95, Lambert 93, RD New and more are built in; anything else takes a+proj=string. The authored camera stays geographic. See Cartographic Maps. Since 0.12.0 a projected map can show satellite or OpenStreetMap imagery too: an imageTileLayeris warped into the projection. - Data in any coordinate system new in 0.12.0: files in a national grid, State Plane or UTM zone — eastings and northings in metres — land in the right place instead of in the ocean. A shapefile’s
.prj, a FlatGeobuf or GeoParquet header, or a GeoJSONcrsmember is read automatically; a CSV declares it on the layer withcrs="EPSG:27700". Common grids and every UTM zone are built in, and any other EPSG code is looked up once at epsg.io. See Data in a local grid. - A globe new in 0.12.0 · preview:
projection="globe"on a map with a basemap draws the Earth as a sphere you can spin, and zooming in turns it into the ordinary flat map. Layers and the camera are written exactly as before, and it switches live. See A globe. - Shaded relief as a layer new in 0.9.1:
<om-layer type="HillshadeLayer" src="terrarium">draws shaded terrain from keyless global elevation tiles on a flat map, orshading="hillshade"turns aCOGLayer/ZarrLayerDEM of your own into relief that shows through a colormap. Up to four suns (sun-azimuth="315 45 135") for Swiss-style multi-directional relief; seamless tile edges, true shape at every latitude, and it works on a projected map. - Geometry your data doesn’t contain new in 0.10.0:
<om-transform>inside a layer addssimplify,smooth,dot-density,buffer(withrepeatandfadefor the engraved coastal vignette of old charts) andcontour(elevation lines marched off a DEM). Sizes are millimetres on the page. - Labels that follow the line they name new in 0.10.0:
<om-layer type="TextOnPathLayer">runs a river’s name along the river. It reads the right way round whichever way the line was drawn, slides to the straightest stretch it fits on 0.10.5, and drops a name that does not fit rather than squeezing it. - Export a map as editable vector new in 0.10.0:
snapshot({ format: "svg" })gives real<path>outlines and real<text>labels for Illustrator or a print shop, with relief and imagery placed as images underneath. - A printed finish in one attribute new in 0.9.0:
<om-effect preset="vintage">treats the rendered frame — seven looks ship, every one built from named operations you can write yourself, sized in millimetres so a 600 dpi export has the same texture you tuned on screen. Aneffectswidget 0.9.1 switches finishes live, and undo sees it. See Print Finishes.
A Full 3D Scene, Still Just Attributes new in 0.3
- Terrain:
terrain="terrarium"adds real elevation with no configuration (keyless AWS tiles). Geographic layers drape onto the surface automatically. You can also give your own DEM tile URL or register presets. - Scene lighting: set
lighting="daylight|studio|flat|custom", withlighting-*attributes for fine control.lighting-sun-datecomputes the sun position from a date and the map location. Alightingwidget shows these as presets and sliders. - Snapshots:
map.snapshot()captures the basemap and the WebGL canvas together, as a PNG data URL or a Blob, or as editable SVG with{ format: "svg" }0.10.0. Use it for print and export pipelines. - Camera limits as author controls 0.8.1 / 0.8.3:
max-pitchcaps how far a map can be tilted andmin-zoomfloors how far it can be zoomed out — for a kiosk or embedded map that should stay within its intended view. Neither is applied automatically. - BIM / IFC models new in 0.6.1:
<om-layer type="BIMLayer" src="./model.ifc">(or theifc-loaderdrop-zone widget, with multi-model federation) parses IFC in the browser via CDN-fetched web-ifc and renders it as 3D Tiles.pick-featuresgives per-element picking on any 3D Tiles layer; declarative isolate/hide/ghost, colour-by-property, anifc-browsermodel tree, afeature-inspector, and anifc-clashoverlay come as attributes and widgets. - Cut, export, and pick inside a 3D scene new in 0.6.2: three tools that work on whatever 3D content is already loaded — 3D Tiles, BIM models, extruded vector geometry alike. A clip box (
<om-map clip-box-min="[lng,lat,elev]" clip-box-max="…">, or theclip-boxwidget with draggable face handles) cuts the scene open;clip-box-invertshows what is outside instead,clip-box-highlightdims the cut-away geometry instead of removing it, andclip="off"opts a layer out. Region export (<om-widget type="draw" export-3d>) downloads the 3D content inside a drawn footprint as a portable GLB, orexport-3d="b3dm"for Cesium and 3D Tiles pipelines.pickable="3d"opts a layer into depth picking, so a click on a building face resolves on the face and carries a real elevation, instead of the ground point behind it. - Snapping to real geometry new in 0.6.2:
<om-map snap="vertex edge midpoint" snap-tolerance="12">refines a click or hover to the nearest vertex, edge, or edge midpoint of whichever feature is under the cursor — including a BIM model’s own wall corners. Vertex wins over midpoint over edge; hold Space to place a point without snapping, the usual CAD convention.snap="off"opts a layer out. - 3D city models new in 0.5: point
dataat a CityJSON file (.city.json, or a streamed.city.jsonl) — national building datasets such as 3DBAG and PLATEAU. The library decodes it to extruded footprints by default, or to real per-face roof geometry with?om-surfaces=1, and reprojects national coordinate grids (NL, CH, DE, JP, AT, SG) automatically.
Data That Stays Fresh
- Formats: JSON/GeoJSON, CSV/TSV, Apache Arrow and GeoArrow (columnar fast path, zstd IPC), Shapefile with
.dbfattributes, KML, GPX, the cloud-native FlatGeobuf (.fgb) and GeoParquet (.parquet) vector formats, and CityJSON / CityJSONSeq (semantic 3D city models). Large parsers load on first use. - GeoTIFF and Cloud-Optimized GeoTIFF new in 0.3.1:
<om-layer type="COGLayer" src="./dem.tif">streams raster tiles by HTTP Range request. The min/max restretch and the colormap are GPU uniforms, so the tiles are not fetched again. Nodata pixels are transparent. - Zarr / GeoZarr rasters new in 0.5.11:
<om-layer type="ZarrLayer" src="./x.zarr" variable="temp" select="time=0">renders chunked N-dimensional array data (climate/weather grids, datacubes) on the GPU. Pin the non-spatial dimensions withselect; GeoZarr stores georeference themselves, a plain Zarr takes manualbounds/crs/spatial-dims.srccan point at any CORS-enabled public remote store — no server setup needed. - Georeferenced drone photos new in 0.5.7:
<om-layer type="ImageOverlay" src="./DJI_0123.jpg" georeference="exif">reads a JPEG’s EXIF/DJI-XMP, computes its WGS84 footprint, and overlays the image — no manual corner-pinning.OmMap.resolveImageOverlay(file)preprocesses once for saved maps; explicitboundsreconstructs without re-reading EXIF. - Live sources: WebSocket streams with upsert-by-key and burst coalescing. Polled REST snapshots with
refresh="5s". - Routing and tracking new in 0.6.7–0.6.11:
<om-layer type="Route">draws a styled route (casing, line, origin/destination markers) from a GeoJSON LineString you already have, or resolves one fromorigin/destinationthrough a pluggableRoutingProvider— a ~15-line adapter covers OSRM’s keyless demo server, Mapbox, OpenRouteService, or your own engine.follow="fit-route"frames the camera on arrival, andom-route-resolvedhands the page the resolved geometry, distance, duration, and legs.<om-layer type="Tracking">renders a moving entity that glides between GPS fixes with bearing-based rotation, anarrow/car/motorcyclemarker tinted per entity, and an optional follow camera; the position feed is ordinary layerdata(a WebSocket stream, a polled endpoint), not a separate API. Link the two withprogress-fromand the route splits live at the marker:tail="none"hides the traveled portion (the client view),tail="dim"darkens it (the operator view). - Multi-dimension GPU filters new in 0.6.5–0.6.6: up to 4 numeric ranges (
filter-fields) and up to 4 categorical keep-lists (filter-category-fields) filter a layer at once, combined on the GPU with no data re-upload. The samefilterwidget renders a slider for a numeric field and a checkbox list for a categorical one, and widget statistics respect every active dimension. - Private endpoints:
OmMap.configureData({ headers, credentials, fetch })keeps credentials out of the markup.
Application UI Without an App Framework
- Widgets: legend, layer switcher, basemap switcher, lighting controls, zoom controls, undo/redo, scale bar, attribution, GPU filter sliders (including readable date labels for epoch-millisecond fields), a play/pause
time-sliderfor temporal data, live Vega-Lite charts, a drawing toolbar, a measure tool, a clip-box control, and an event-drivendynamic-chart. You can also write custom widgets with a typed context (ctx.data,ctx.stats,ctx.viewport,ctx.emit). - Measurement, including cut/fill volume new in 0.6.2:
<om-widget type="measure" modes="distance area volume">measures geodesic distance and area, and — involumemode — real earthworks cut and fill. Outline a footprint and the library integrates the terrain DEM cell by cell against a base surface, so one shape can report cut and fill on sloping ground, each with a published ± error quoted against the DEM’s own ground-sample distance. Choose the base surface withbase-surface(customdrag-a-plane,triangulatedboundary TIN,plane,lowest,highest,average).density,swell, andshrinkadd Bank/Loose/Compacted volumes and tonnage;profilesamples an elevation profile around the footprint for adynamic-chartto plot. Volume mode needsterrainon the map. - Safe date formatting:
formatDate($time, 'datetime', 'UTC')turns epoch-millisecond or ISO fields into readableget-textlabels without enabling arbitrary JavaScript. The same date style and time-zone contract is available on the built-in filter widget. - Managed, adaptive layout new in 0.4:
positionputs a widget in one of 8 logical slots (top-start…bottom-end), and the library sets the pixel position. Widgets in the same slot stack with a shared gap, and small controls next to each other join into one group. The map attribution and the license badge sit in the bottom slots as normal members, so they do not cover a widget. On narrow maps (640px or less by default; change it with--om-widget-fold-breakpoint) the widgets fold into drawers, and a slot dims when an open popup covers it.<om-map widgets-hidden>hides all your widgets but keeps them in the page, andposition="manual"turns off management for one widget. - Classified symbology new in 0.6.12:
classify-by="magnitude"computes choropleth-style class breaks from the data itself —classify-scale="quantile|equal-interval|jenks", 2–12 classes, and 14 named color ramps (viridis,turbo,ylorrd,spectral, …) — and the legend shows the computed classes automatically. No hand-written domains or color lists. - Temporal playback new in 0.6.12:
<om-widget type="time-slider" layer="quakes" field="time" duration="20s" loop>plays a time field back through the ordinary GPU filter — cumulative reveal or a slidingwindow— so statistics, undo, and stories stay coherent with the animation. - Symbology-aware legends new in 0.3: the legend uses each layer’s real color styling. A continuous
scale()ramp becomes a gradient bar with domain labels. A threshold scale becomes discrete class ranges. A categorical expression becomes a swatch palette. A raster colormap becomes its ramp. You do not configure the legend. - Themeable from plain CSS: each built-in widget reads
--om-widget-*custom properties (om-map { --om-widget-bg: #111827; --om-widget-fg: #f9fafb; }). You can set a dark theme with one CSS rule and no JavaScript. - Overlays and behaviors: HTML popups anchor to map coordinates or to a picked feature. Declarative
on="click|hover|drag|load"interactions need no event-handler code. - Map stories: an
<om-story>has steps that fire the same actions as behaviors, including basemap, lighting, and terrain changes. A player widget plays the steps, and you can scrub to any step. - Travel-map animations + external renderer support new in 0.6.18: trace steps take
follow(camera rides the drawing tip) andeasing;anchor="surface"puts labels on the shapes themselves; andwhenSettled()+ interpolatingseek()+ thedata-om-recordingdeterminism switch let Remotion-class frameworks render stories frame-by-frame with byte-identical output. - Sharp 3D flybys + video export new in 0.6.17:
<om-story warm-tiles>pre-loads a route’s 3D tiles before playback;<om-story paced>holds each frame until tiles are sharp (no blurry frame, ever); andnpx onlymapjs record map.html --out flyby.mp4turns a story into a video with every frame fully refined —--gpufor several-times-faster takes on heavy scenes.
Built for Verification
OmMap.validate(html)returns structured errors, and each error has a fix. Add thevalidateattribute for a live error panel on the page.- Headless test harness:
mountForTestruns full behavioral tests (picks, widgets, overlays, real projection math) in jsdom or happy-dom, with no WebGL. - Layout audit new in 0.4:
npx @nika-js/onlymap check-layout <manifest.html>checks the real widget layout in headless Chromium at several viewport widths. It confirms that no widgets overlap, that each widget is hit-testable and in its slot, and that same-slot edges align. It prints structured{severity, element, message, fix}diagnostics and exits 0 or 1. You can also importauditLayout(page)into your own Playwright suite. - React adapter:
@nika-js/onlymap/reactgives<OmMap>,<OmLayer>,<OmWidget>,<OmOverlay>, and a typeduseOmMap()hook over the same core. React keeps the state, and the adapter does not conflict with the virtual DOM. - Custom layer classes:
@nika-js/onlymap/deckre-exports the bundled deck.gl classes.OmMap.registerLayerextensions use the renderer’s own class hierarchy. A desktop app can register domain layers (local-file rasters, catalog sources) as first-class manifest types.
Framework & Programmatic Integration new in 0.4
MapController: a programmatic front-end for frameworks. TypedLayerDescriptors drive the same reconcile core, with no DOM manifest. It adds camera methods,emit,injectPick,snapshot, the picking queriespickAt/pickRect/unprojectwith apickModeswitch 0.11.5 (also on<om-map>), andonViewChange/onMapPoint/onTilesetLoadcallbacks.- External-store contract:
getStore(token)gives a{subscribe, getSnapshot}store for each token (viewport,selection,layers,data:<id>). The snapshots are cached, immutable, and tagged with anorigin. You can use them directly with React’suseSyncExternalStore, MobX, Redux, or Zustand. - Consumer events:
om-view-changedfires when the camera settles.om-map-pointfires on each click or hover with the map coordinate — use it for custom capture tools that the draw widget does not cover.om-tileset-loadgives aTile3DLayer’s liveTileset3D, for tools such as region export.om-route-resolvedhands over aRoutelayer’s resolved path — normalized geometry, distance, duration, legs, and bounds — so a page can render an ETA or a turn list without re-fetching the route.om-measurecarries the measure widget’s full readout — distance, area, cut/fill volumes with their error bounds, material-adjusted figures, and the elevation profile series — so you can render your own readout instead of the built-in panel.
Roadmap
These features are designed but not shipped yet: Mapbox GL basemaps, depth-interleaved 3D compositing, a globe without a basemap (and text, icon and arc layers on a globe), SSE transport,dblclick behaviors, the typed fluent builder, and stories and draw as React components. On cartography: relief cannot yet be masked to land, contour samples in the DEM’s Mercator grid even on a projected sheet (the lines themselves project exactly), and native-CRS tile pyramids are deferred. The README lists the current state in its “Not implemented yet” section at each release.
Resources
- npm:
@nika-js/onlymap - GitHub (docs, examples, llms.txt): NikaGeospatial/onlymapjs
- Product page: nikaplanet.com/onlymap