skill
ng-mapcn
Beautiful maps with shadcn/ui for Angular ❤️
Set up
Skills are installed by placing their folder where your agent loads skills.
The download comes from github.com/juanvieiraprado99/ng-mapcn itself, not from our server, so what you get is exactly what its author published.
Install instructions come from the README
README
synced Oct 22, 2026ng-mapcn
ng-mapcn is a set of composable MapLibre GL map components for Angular. It is built for Angular apps that need theme-aware maps (markers, popups, routes, GeoJSON, arcs, clusters, heatmaps, raster/vector tile sources, image overlays, 3D buildings and terrain, draw tools) without API keys or a paid tile provider. It is an Angular port of the mapcn API, with MapLibre GL as the rendering engine and the copy-paste ownership model of shadcn/ui: ng add ng-mapcn copies the sources into your project so you own the code.
Docs · Blocks gallery · Playground · Installation · API reference
Features
- MapLibre GL JS v6 under the hood, no API keys required (Carto basemaps)
- Theme-aware: follows the
dark/lightclass ordata-themeattribute on<html>(orprefers-color-scheme) and swaps the basemap at runtime - Composable: declare markers, popups, routes, GeoJSON, arcs, clusters, heatmaps, tile sources (raster, vector, PMTiles), image overlays, 3D buildings and terrain as children of
<ng-map> - Signals-first: standalone components, signal inputs/outputs,
effect()-based reconciliation - Copy-paste distribution:
ng add ng-mapcncopies the sources into your project (shadcn-style). Also usable as a regular npm package. - SSR-safe: nothing touches
windowor MapLibre during server render - AI-ready: an agent skill (
npx skills add juanvieiraprado99/ng-mapcn) plusllms.txtteach coding agents the real API
Compatibility
| ng-mapcn | Angular | MapLibre GL | npm tag |
|---|---|---|---|
22.x | ^22.0.0 | ^6.11.2 | latest, beta |
21.x | ^21.2.9 | ^5.16.0 | n/a |
18.x | ^18.2.14 | ^5.16.0 | n/a |
Node.js ^22.22.3 || ^24.15.0 || >=26.0.0. The commands below use the @beta tag so you always get the newest 22.x prerelease.
Quick start
1. Install
Copy-paste (recommended, shadcn-style):
ng add ng-mapcn@beta
This will:
- Add
maplibre-glto your dependencies (and@types/geojsonto your dev dependencies) - Register
maplibre-gl/dist/maplibre-gl.cssinangular.json - Register MapLibre's worker files as build assets (see MapLibre worker)
- Append the required
.maplibregl-popup-*CSS overrides to your global stylesheet - Copy the component sources to
src/app/components/ui/map(customize with--path)
If your app already depends on an older maplibre-gl, ng add keeps your version and warns: update it to ^6.11.2.
Drawing (ng-map-draw) is opt-in so apps that do not draw never install terra-draw: run ng add ng-mapcn@beta --draw to also copy the draw sources to <path>/draw (import from there) and add terra-draw and terra-draw-maplibre-gl-adapter to your dependencies.
Or as a regular npm package:
npm install ng-mapcn@beta maplibre-gl
To draw with ng-map-draw, also install its optional peer dependencies and import from the ng-mapcn/draw entry point (the core ng-mapcn entry never loads terra-draw):
npm install terra-draw terra-draw-maplibre-gl-adapter
Then in your global styles.scss:
// theme tokens + component utility classes
@use 'ng-mapcn/src/lib/styles/index' as *;
// required .maplibregl-popup-* overrides
@use 'ng-mapcn/styles.scss' as *;
// MapLibre base CSS
@import 'maplibre-gl/dist/maplibre-gl.css';
The popup overrides are required. MapLibre injects its own popup chrome, which must be neutralized so the styled content shows cleanly:
.maplibregl-popup-content {
background: transparent !important;
box-shadow: none !important;
padding: 0 !important;
border-radius: 0 !important;
max-width: none !important;
}
.maplibregl-popup-tip {
display: none !important;
}
MapLibre worker
MapLibre GL v6 ships as ES modules and loads its web worker from maplibre-gl-worker.mjs next to the bundle that imports it, and that worker imports maplibre-gl-shared.mjs. Angular's builder does not emit these files on its own, so copy both to the output root (ng add does this for you):
// angular.json → projects.<app>.architect.build.options.assets
{ "glob": "maplibre-gl-worker.mjs", "input": "node_modules/maplibre-gl/dist", "output": "/" },
{ "glob": "maplibre-gl-shared.mjs", "input": "node_modules/maplibre-gl/dist", "output": "/" }
Bundlers cannot resolve MapLibre's default worker location (import.meta.url); under ng serve it points into Vite's prebundled deps and the console shows Worker failed to load. So, before creating the first map, <ng-map> points MapLibre at maplibre-gl-worker.mjs resolved against <base href>, which works in ng serve and production builds alike. If you serve the files from somewhere else (a CDN, a deployUrl), call MapLibre's own setWorkerUrl('https://…/maplibre-gl-worker.mjs') before the first map is created; <ng-map> keeps a URL you already set. MapLibre v6 requires WebGL2: without it the Map constructor throws a GPUInitializationError, which <ng-map> emits through (mapError).
2. Add a map
A complete standalone component (with ng add, import from ./components/ui/map instead of 'ng-mapcn'):
import { Component } from '@angular/core';
import { MapComponent, MapMarkerComponent, MarkerPopupComponent } from 'ng-mapcn';
@Component({
selector: 'app-map',
imports: [MapComponent, MapMarkerComponent, MarkerPopupComponent],
template: `
<div class="map-container">
<ng-map [center]="[-46.63, -23.55]" [zoom]="10">
<ng-map-marker [longitude]="-46.63" [latitude]="-23.55">
<ng-marker-popup><strong>São Paulo</strong></ng-marker-popup>
</ng-map-marker>
</ng-map>
</div>
`,
styles: `
.map-container {
height: 400px;
}
`,
})
export class AppMap {}
The map fills its parent, so give the parent an explicit height. More building blocks (controls, routes, GeoJSON, arcs, clusters, heatmaps, 3D) are listed below and in the blocks gallery.
Components
| Component | Description |
|---|---|
ng-map | Map host. Inputs: center, zoom, theme (light/dark/auto), styles (per-theme basemap), blank (tile-less dataviz style), projection, viewport (controlled mode), loading, options (raw MapLibre MapOptions). Outputs: viewportChange, mapReady, mapClick, mapError. |
ng-map-controls | Zoom / compass / locate / fullscreen buttons (custom UI, not MapLibre IControl). |
ng-map-marker | Declarative marker. longitude, latitude, draggable, offset, rotation, anchor, alignments, ariaLabel; click/hover/drag outputs. Keyboard: Enter/Space click, arrow keys drag. |
ng-marker-content | Projects custom HTML into the marker element (empty = default dot). |
ng-marker-popup / ng-marker-tooltip / ng-marker-label | Popup on click, tooltip on hover or focus, CSS label above/below the marker. |
ng-map-popup | Standalone popup at a lng/lat (no marker). |
ng-map-route | LineString layer with color/width/opacity/dashArray and click output. |
ng-map-geojson | Fill + outline layers from GeoJSON data or URL. Declarative hover via promoteId + fillHoverPaint (feature-state). |
ng-map-arc | Curved arcs between points (flight paths). Invisible hit layer for comfortable hover/click; hoverPaint via feature-state. |
ng-map-cluster-layer | Clustered points with count labels, threshold-based colors/sizes and expansion-zoom on click. |
ng-map-heatmap | Point-density heatmap layer with a theme-aware gradient, weightProperty/weightRange, zoom-dependent radius/intensity/opacity expressions and maxZoom. |
ng-map-raster-source | Raster tile source (XYZ/TMS/WMS) plus its raster layer: tiles/url, tileSize, opacity, visible, beforeId. |
ng-map-vector-source | Vector tile source (MVT, TileJSON, pmtiles://), source only; draw it with layers that wait for it via MapContextService.hasSource(id). |
ng-map-image-overlay | Image pinned to four corner coordinates, with opacity, visible and beforeId; url and coordinates update in place. |
ng-map-fill-extrusion | 3D extrusion of polygons (3D buildings): defaults to the Carto basemap buildings (protomaps / buildings, height), or any source by id; color, opacity, minZoom, filter, beforeId. |
ng-map-terrain | 3D terrain from a raster-dem source (public Terrarium tiles by default) with exaggeration and an optional hillshade; re-applied after a theme switch. |
ng-map-draw | Draw, select and edit points, lines and polygons (terra-draw). Optional ng-mapcn/draw entry point: two-way GeoJSON features, mode, drawEnd/featureEdit/featureSelect, theme-aware, restarts across theme switches. Needs terra-draw and terra-draw-maplibre-gl-adapter. |
ng-map also takes [fitBounds]="{ padding, maxZoom, duration, animate, bounds? }" to fit the camera to its children's data (or explicit bounds) once loaded and whenever they change; MapContextService.fitBounds() is the imperative form.
Utilities: MapViewport, getViewport, buildArcCoordinates, mergeHoverPaint, blankMapStyle, defaultStyles, and MapContextService (map / isLoaded / resolvedTheme signals and the hasSource(id) source registry for advanced layer work via inject).
Theming
Without a theme input, the map watches the dark/light class (or a data-theme="dark"/"light" attribute) on document.documentElement (works with any theme toggler) and falls back to prefers-color-scheme. Switching theme calls map.setStyle(..., { diff: true }) and every layer component re-adds itself automatically once the style settles.
Basemaps
Any MapLibre-compatible style works. Set it per map with styles, or once for the whole app with provideMapStyles():
import { provideMapStyles } from 'ng-mapcn';
export const appConfig: ApplicationConfig = {
providers: [
provideMapStyles({
light: 'https://tiles.openfreemap.org/styles/positron',
dark: 'https://tiles.openfreemap.org/styles/dark',
}),
// ...or pass a Signal (or a factory that injects a service) to swap the
// basemap at runtime: provideMapStyles(() => inject(BasemapService).styles)
],
};
Precedence: [styles] → [blank] → provideMapStyles() → Carto default.
Labels and translations
Control tooltips, the loader and the popup close button are English by default. Override them app-wide with provideMapLabels() (static value, signal or factory; missing keys keep the English default), or per component with the labels input on ng-map, ng-map-controls and ng-map-popup:
import { provideMapLabels } from 'ng-mapcn';
provideMapLabels({
zoomIn: 'Aproximar',
zoomOut: 'Afastar',
loading: 'Carregando mapa',
closePopup: 'Fechar',
});
// follow the active locale: provideMapLabels(() => inject(I18nService).mapLabels)
Keys: zoomIn, zoomOut, resetNorth, locate, fullscreen, loading, closePopup, marker.
| Provider | Style URL | API key |
|---|---|---|
| Carto (default) | https://basemaps.cartocdn.com/gl/{positron|dark-matter|voyager}-gl-style/style.json | no |
| OpenFreeMap | https://tiles.openfreemap.org/styles/{liberty|bright|positron|dark|fiord} | no |
| OpenStreetMap | raster tiles https://tile.openstreetmap.org/{z}/{x}/{y}.png — build the StyleSpecification by hand; dev/demo only (tile usage policy) | no |
| MapTiler | https://api.maptiler.com/maps/{streets-v4|streets-v4-dark|dataviz-v4}/style.json?key=KEY | yes |
| Stadia Maps | https://tiles.stadiamaps.com/styles/{alidade_smooth|alidade_smooth_dark}.json?api_key=KEY | keyless on localhost |
Live examples for each: ng-mapcn demo → Map Styles.
SSR support
ng-mapcn is server-side-rendering safe. The map is created in afterNextRender
(browser-only, after hydration) and every child component (markers, popups,
layers, controls) gates its work on the map signal, which is null on the
server — so nothing touches document, window, or MapLibre during server
render. On the server the map host renders a plain container <div> plus the
loader, which matches the first client render and hydrates cleanly.
In a consumer app with @angular/ssr, just enable hydration:
// app.config.ts
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withEventReplay()),
// ...your other providers
],
};
Notes:
- Theme flash: on the server the theme resolves to
light(nomatchMedia); if the user is in dark mode the basemap swaps once on the client after hydration. This mirrors the Reactmapcnbehavior. Set thethemeinput explicitly to avoid it. - If a bundler complains about
maplibre-glduring server build/prerender, mark it external for the server (externalDependencies: ["maplibre-gl"]) or addngSkipHydrationto the element hosting<ng-map>.
Security
- Only load trusted basemap styles and attributions. The attribution control (on by default) renders style and source attribution strings as HTML, so the URLs you pass to
[styles]/provideMapStyles(), acustomAttributionin[options], and GeoJSON URLs given to[data]must come from sources you control or trust, never straight from user input. ng-mapcn requiresmaplibre-gl^6.11.2, which includes the fix for GHSA-jrc7-96c5-q579 (attribution sanitizer bypass inmaplibre-gl≤ 6.4.0). - Content Security Policy: with the worker self-hosted as above,
worker-src 'self'is enough (noblob:). MapLibre also needsimg-src data: blob: 'self'plus your tile/style hosts inconnect-srcandimg-src(https://basemaps.cartocdn.com https://*.basemaps.cartocdn.comfor the default basemaps: the style is served from the first; TileJSON, glyphs, sprites and tiles fromtiles.basemaps.cartocdn.comandtiles-a/b/c.basemaps.cartocdn.com). - Privacy: the default basemaps are served by Carto, a third party that receives your users' IP address and the areas they view. Self-host or swap the style with
[styles]/provideMapStyles()if that matters.ng-map-controls' locate button asks for the browser's geolocation and emits the coordinates through(locateChange): treat them as personal data.
Performance
- Lazy-load the map. MapLibre is large: in the demo's production build its chunk is about 1 MB (≈285 KB gzipped), more than the demo's entire initial JavaScript (≈160 KB gzipped), and the first map also fetches the worker files (≈150 KB gzipped, on the worker thread, cached afterwards). Keep
ng-mapcnout of your initial bundle with a lazy route (loadComponent) or@defer (on viewport) { <ng-map>…</ng-map> }. - Many points → layers, not markers. Each
<ng-map-marker>is a DOM element with its own component; for hundreds of points use<ng-map-cluster-layer>,<ng-map-heatmap>or<ng-map-geojson>, which render on the GPU. - Pass new references to update data. Layer components push
[data]/[coordinates]to MapLibre only when the reference changes; replace arrays and objects instead of mutating them.
For LLMs
The docs site serves llms.txt (index) and llms-full.txt (every component, input, output and default in one file), following the llmstxt.org convention. Point your coding agent at either instead of scraping the docs.
AI agent skill
ng-mapcn ships an agent skill that teaches coding agents (Claude Code, Cursor, Codex, GitHub Copilot, Windsurf, Gemini and others) the library's real API: component selectors, inputs and outputs, setup for both install channels, copy-paste recipes and common pitfalls. Without it, agents tend to guess class names and selectors that do not exist.
Install it with the skills.sh CLI, from your app's root:
npx skills add juanvieiraprado99/ng-mapcn
The CLI downloads the skill and configures it for your agent. Pass --list to see the skills in the repository without installing, or --skill ng-mapcn to install this one explicitly.
Contributing
Contributions are welcome. Please read the contributing guide and the code of conduct. To report a vulnerability, follow the security policy instead of opening a public issue.
To run the demo locally:
npm install
npm run build-ui-dev # builds the lib, installs it locally and serves the demo
Credits
- mapcn (docs) — the React + MapLibre GL library that inspired this project. ng-mapcn is an Angular port of its composition model and component API. All credit for the original design goes to its authors.
- Zard UI (docs) — the Angular component library (shadcn-style, copy-paste) used to build this project's demo & documentation site.
- MapLibre GL JS — the rendering engine.
- Carto — the default free basemaps.
License
MIT © ng-mapcn. See the LICENSE file.