npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ng-mapcn

v22.0.0-beta.9

Published

Beautiful maps for Angular, made simple. MapLibre GL components for Angular 22+ with theme-aware styling inspired by shadcn/ui.

Downloads

1,055

Readme

ng-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, symbol labels and icons, 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 · Testing · API reference

Features

  • MapLibre GL JS v6 under the hood, no API keys required (Carto basemaps)
  • Theme-aware: follows the dark/light class or data-theme attribute on <html> (or prefers-color-scheme) and swaps the basemap at runtime
  • Composable: declare markers, popups, routes, GeoJSON, arcs, clusters, heatmaps, symbol labels and icons, 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-mapcn copies the sources into your project (shadcn-style). Also usable as a regular npm package.
  • SSR-safe: nothing touches window or MapLibre during server render
  • Testable: a MapLibre fake and Angular CDK harnesses (ng-mapcn/testing) let you unit-test maps in jsdom without WebGL
  • AI-ready: an agent skill (npx skills add juanvieiraprado99/ng-mapcn) plus llms.txt teach 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

ng add installs with your workspace's package manager (npm, pnpm, yarn or bun), so run it as npx ng add, pnpm ng add, yarn ng add or bunx ng add as you prefer.

This will:

  1. Add maplibre-gl to your dependencies (and @types/geojson to your dev dependencies)
  2. Register maplibre-gl/dist/maplibre-gl.css in angular.json
  3. Register MapLibre's worker files as build assets (see MapLibre worker)
  4. Append the required .maplibregl-popup-* CSS overrides to your global stylesheet
  5. 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
# or: pnpm add / yarn add / bun add 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
# or: pnpm add / yarn add / bun add 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, symbols, 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. Rendered inside MapLibre's corner containers, so it stacks with the scale, legend, minimap and style switcher. Breaking change: the fixed bottom offsets are gone; style the card with className, not host-descendant selectors. | | ng-map-scale | Scale bar as a MapLibre control. unit (metric/imperial/both), maxWidth, position. | | ng-map-minimap | Overview map as a MapLibre control (default bottom-left). Follows the main viewport at zoomOffset (5) levels lower, draws the viewport footprint, and click, drag or arrow keys navigate the main map. Mirrors the main basemap unless styles/blank is set. width, height, [(collapsed)]; collapsing frees its WebGL context. | | ng-map-legend | Legend card: category swatches and colour gradients from sections. title, collapsible, [(collapsed)], position; content projects below. | | ng-map-style-switcher | Basemap listbox. options, [(value)], position; the selected option overrides provideMapStyles() and [styles] for that map. | | 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 (a named, non-modal role="dialog" card; the marker gets aria-haspopup="dialog", aria-expanded and, while open, aria-controls), tooltip on hover or focus, CSS label above/below the marker. | | ng-map-popup | Standalone popup at a lng/lat (no marker), a named, non-modal role="dialog" card. | | ng-map-route | LineString layer with color/width/opacity/dashArray and click output. | | ng-route-playback / ng-route-playback-control | Animates the route progress (overrides [progress]): [(playing)], [(progress)], [(speed)], duration, loop, follow, ended; play()/pause()/toggle()/restart()/seek(). The control (child of ng-map, [for]="pb") adds play/pause, a scrubber and a speed selector as a corner control. | | 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-symbol-layer | Icons and text labels for GeoJSON points: images registered by id, data-driven icon, textField, collision-aware, allowOverlap, filter, featureClick/featureHover. | | 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: ng-map-style-switcher selection → [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, ng-map-scale, ng-map-legend, ng-map-style-switcher, ng-map-minimap, ng-route-playback-control, ng-map-marker, ng-map-popup and ng-marker-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, popup, marker, scale, legend, showLegend, hideLegend, mapStyle, minimap, showMinimap, hideMinimap.

| 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 (no matchMedia); if the user is in dark mode the basemap swaps once on the client after hydration. This mirrors the React mapcn behavior. Set the theme input explicitly to avoid it.
  • If a bundler complains about maplibre-gl during server build/prerender, mark it external for the server (externalDependencies: ["maplibre-gl"]) or add ngSkipHydration to 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(), a customAttribution in [options], and GeoJSON URLs given to [data] must come from sources you control or trust, never straight from user input. ng-mapcn requires maplibre-gl ^6.11.2, which includes the fix for GHSA-jrc7-96c5-q579 (attribution sanitizer bypass in maplibre-gl ≤ 6.4.0).
  • Content Security Policy: with the worker self-hosted as above, worker-src 'self' is enough (no blob:). MapLibre also needs img-src data: blob: 'self' plus your tile/style hosts in connect-src and img-src (https://basemaps.cartocdn.com https://*.basemaps.cartocdn.com for the default basemaps: the style is served from the first; TileJSON, glyphs, sprites and tiles from tiles.basemaps.cartocdn.com and tiles-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-mapcn out 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.

Testing your maps

Unit-test components that render <ng-map> in jsdom, without WebGL. ng-mapcn imports maplibre-gl statically, so the test runner replaces that module with a fake that ships in the package (ng-mapcn/testing/maplibre). It records calls, fires events on demand and renders markers, popups and controls into the map container.

// src/testing/setup.ts (Vitest)
import { configureFakeMapLibre, fakeRegistry } from 'ng-mapcn/testing/maplibre';
import { beforeEach, vi } from 'vitest';

vi.mock('maplibre-gl', () => import('ng-mapcn/testing/maplibre'));
configureFakeMapLibre({ spy: vi.fn });
beforeEach(() => fakeRegistry.reset());
// vitest.config.mts: process ng-mapcn itself so the mock reaches it
export default defineConfig({ test: { server: { deps: { inline: [/ng-mapcn/] } } } });
import { loadMap, renderMap, getFakeMap, structuralCalls } from 'ng-mapcn/testing';

const { fixture } = await renderMap(
  '<ng-map><ng-map-route id="trip" [coordinates]="c" /></ng-map>',
  {
    imports: [MapRouteComponent],
    hostProps: {
      c: [
        [0, 0],
        [1, 1],
      ],
    },
  },
);
await loadMap(fixture);
expect(structuralCalls(getFakeMap(fixture))).toEqual(['addSource:trip', 'addLayer:trip-layer']);
  • ng-mapcn/testing: renderMap, loadMap, reloadStyle, getFakeMap, fireMapEvent, structuralCalls and friends, plus MAP_TEST_SELECTORS, the stable DOM selectors of a rendered map for your own Playwright or Cypress tests.
  • ng-mapcn/testing/harness: Angular CDK component harnesses (NgMapHarness, NgMapMarkerHarness, NgMapPopupHarness, NgMapControlsHarness). @angular/cdk is an optional peer dependency, only needed for this entry point. NgMapMarkerHarness.isPopupOpen() reads the marker's aria-expanded (or, when the marker content is its own button, that button's, mirrored from <ng-marker-popup #info> with [attr.aria-expanded]="info.isOpen()"), so it reports that marker's own popup.
  • Vitest is supported first-class. Jest works by mapping maplibre-gl to the fake with moduleNameMapper, but ng-mapcn's own tests do not cover it. Karma/Jasmine are not supported (they cannot mock an ES module).
  • ng add does not copy the testing files: install the ng-mapcn package to use them.
  • Change host values after rendering with update() (it marks the host for check, as zoneless apps need) or signals, call renderMap() once per test, and pair fake timers with configureMapTesting({ advanceTimers }) (otherwise loadMap() throws).

Setup for Vitest and Jest, fake timers, the harnesses and the limitations: Testing your maps.

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.