map

Beautiful maps built on MapLibre GL, themed with zard. Copy and paste into your apps.

PreviousNext
Loading map
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardMapImports } from '@/shared/components/map/map.imports';

@Component({
  selector: 'z-demo-map-preview',
  imports: [ZardMapImports],
  template: `
    <div class="h-[420px] w-full overflow-hidden rounded-xl border">
      <ng-map [center]="[-46.6333, -23.5505]" [zoom]="11">
        <ng-map-controls />
        <ng-map-marker [longitude]="-46.6333" [latitude]="-23.5505">
          <ng-marker-content />
          <ng-marker-tooltip>Praça da Sé</ng-marker-tooltip>
          <ng-marker-popup>
            <p class="font-medium">São Paulo</p>
            <p class="text-muted-foreground">The largest city in Brazil.</p>
          </ng-marker-popup>
        </ng-map-marker>
      </ng-map>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapPreviewComponent {}

Credits

The map is powered by ng-mapcn on top of MapLibre GL, and ng-mapcn is an Angular port of mapcn. Zard themes it and adds the basemap presets and the picker.

  • ng-mapcn The Angular map components this item is built on (MIT).
  • MapLibre GL JS The rendering engine (BSD-3-Clause).
  • mapcn The React library whose component API and look ng-mapcn ports.

ng-mapcn is still pre-1.0 upstream and ships betas only, so zard pins it to an exact version. Updating it is a deliberate step. Basemap attribution, such as © OpenStreetMap and © CARTO, stays visible on every map.

Installation

1

Run the CLI

Use the CLI to add map to your project.

Copy
npx zard-cli@latest add map
2

Register map to your project

app.routes.ts
Copy
import { Routes } from '@angular/router';
import { provideZardMap } from '@/shared/components/map/map.provider';

export const routes: Routes = [
  {
    path: 'map',
    loadComponent: () => import('./map.page').then(m => m.MapPage),
    // Register it on the lazy route that renders the maps: ng-mapcn pulls in all of MapLibre (about 1 MB),
    // so keep it out of app.config.ts and the initial bundle of every other route.
    providers: [provideZardMap({ persist: true })],
  },
];

Usage

import { ZardMapImports } from '@/shared/components/map/map.imports';
Copy
<div class="h-96 overflow-hidden rounded-xl border">
  <ng-map [center]="[-46.63, -23.55]" [zoom]="10">
    <ng-map-controls />
    <ng-map-marker [longitude]="-46.63" [latitude]="-23.55">
      <ng-marker-content />
      <ng-marker-popup>São Paulo</ng-marker-popup>
    </ng-map-marker>
  </ng-map>
</div>
Copy

Examples

basic

A map fills its parent, so give the parent a height. The basemap is Carto Positron in light mode and Dark Matter in dark mode: toggle the theme and it swaps with the page.
Loading map
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardMapImports } from '@/shared/components/map/map.imports';

@Component({
  selector: 'z-demo-map-basic',
  imports: [ZardMapImports],
  template: `
    <div class="h-[420px] w-full overflow-hidden rounded-xl border">
      <ng-map [center]="[2.3522, 48.8566]" [zoom]="11" />
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapBasicComponent {}

controls

ng-map-controls adds zoom, compass, locate and fullscreen buttons in any corner. They are plain HTML styled with the zard tokens.
Loading map
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { ZardMapImports } from '@/shared/components/map/map.imports';
import {
  ZardToggleGroupComponent,
  type ZardToggleGroupItem,
} from '@/shared/components/toggle-group/toggle-group.component';

type ControlsPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';

@Component({
  selector: 'z-demo-map-controls',
  imports: [ZardMapImports, ZardToggleGroupComponent],
  template: `
    <div class="flex w-full flex-col gap-3">
      <z-toggle-group
        zMode="single"
        zType="outline"
        zSize="sm"
        [zItems]="positions"
        [zDefaultValue]="position()"
        (valueChange)="onPositionChange($event)"
      />

      <div class="h-[420px] w-full overflow-hidden rounded-xl border">
        <ng-map [center]="[139.6917, 35.6895]" [zoom]="10">
          <ng-map-controls
            [position]="position()"
            [showZoom]="true"
            [showCompass]="true"
            [showLocate]="true"
            [showFullscreen]="true"
          />
        </ng-map>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapControlsComponent {
  protected readonly position = signal<ControlsPosition>('bottom-right');

  protected readonly positions: ZardToggleGroupItem[] = [
    { value: 'top-left', label: 'Top left' },
    { value: 'top-right', label: 'Top right' },
    { value: 'bottom-left', label: 'Bottom left' },
    { value: 'bottom-right', label: 'Bottom right' },
  ];

  protected onPositionChange(value: string | string[]): void {
    if (typeof value === 'string' && value) {
      this.position.set(value as ControlsPosition);
    }
  }
}

markers

A marker can carry its own content, a label, a popup that opens on click and a tooltip that shows on hover or focus. Drag the red one, or focus it and use the arrow keys. Add ng-marker-content for the default dot.
Loading map
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { provideIcons, NgIcon } from '@ng-icons/core';
import { lucideCoffee, lucideLandmark, lucideMapPin } from '@ng-icons/lucide';

import { ZardBadgeComponent } from '@/shared/components/badge/badge.component';
import { ZardMapImports } from '@/shared/components/map/map.imports';

@Component({
  selector: 'z-demo-map-markers',
  imports: [ZardMapImports, NgIcon, ZardBadgeComponent],
  template: `
    <div class="relative h-[420px] w-full overflow-hidden rounded-xl border">
      <ng-map [center]="[-9.1393, 38.7223]" [zoom]="13">
        <ng-map-controls />

        <!-- Default dot. The tooltip shows on hover and on keyboard focus. -->
        <ng-map-marker [longitude]="-9.1333" [latitude]="38.7139" ariaLabel="Alfama">
          <ng-marker-content />
          <ng-marker-tooltip>Alfama</ng-marker-tooltip>
        </ng-map-marker>

        <!-- Custom content and a label under it. -->
        <ng-map-marker [longitude]="-9.1459" [latitude]="38.7103" ariaLabel="Café A Brasileira">
          <ng-marker-content>
            <div
              class="bg-primary text-primary-foreground border-background flex size-9 items-center justify-center rounded-full border-2 shadow-lg"
            >
              <ng-icon name="lucideCoffee" class="size-4" aria-hidden="true" />
            </div>
          </ng-marker-content>
          <ng-marker-label position="bottom">A Brasileira</ng-marker-label>
        </ng-map-marker>

        <!-- A popup opens on click. Its content is plain HTML, so zard components fit right in. -->
        <ng-map-marker [longitude]="-9.1598" [latitude]="38.6916" ariaLabel="Torre de Belém">
          <ng-marker-content>
            <div
              class="bg-background text-foreground flex size-9 items-center justify-center rounded-full border shadow-lg"
            >
              <ng-icon name="lucideLandmark" class="size-4" aria-hidden="true" />
            </div>
          </ng-marker-content>
          <ng-marker-popup [closeButton]="true">
            <div class="flex flex-col gap-1.5 pr-4">
              <p class="font-medium">Torre de Belém</p>
              <p class="text-muted-foreground text-xs">A 16th century fortress on the Tagus river.</p>
              <z-badge zType="secondary">UNESCO World Heritage</z-badge>
            </div>
          </ng-marker-popup>
        </ng-map-marker>

        <!-- Drag it, or focus it and use the arrow keys. -->
        <ng-map-marker
          [longitude]="position().lng"
          [latitude]="position().lat"
          [draggable]="true"
          ariaLabel="Draggable marker"
          (dragEnd)="position.set($event)"
        >
          <ng-marker-content>
            <div
              class="bg-destructive border-background flex size-9 cursor-grab items-center justify-center rounded-full border-2 text-white shadow-lg"
            >
              <ng-icon name="lucideMapPin" class="size-4" aria-hidden="true" />
            </div>
          </ng-marker-content>
          <ng-marker-tooltip>Drag me</ng-marker-tooltip>
        </ng-map-marker>
      </ng-map>

      <z-badge zType="secondary" class="absolute top-3 left-3 font-mono">
        {{ position().lng.toFixed(4) }}, {{ position().lat.toFixed(4) }}
      </z-badge>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  viewProviders: [provideIcons({ lucideCoffee, lucideLandmark, lucideMapPin })],
})
export class ZardDemoMapMarkersComponent {
  protected readonly position = signal({ lng: -9.1393, lat: 38.7223 });
}

popup

ng-map-popup sits at any coordinate, with no marker. Bind [(open)] to open and close it from your own UI.
Loading map
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardMapImports } from '@/shared/components/map/map.imports';

@Component({
  selector: 'z-demo-map-popup',
  imports: [ZardMapImports, ZardButtonComponent],
  template: `
    <div class="relative h-[420px] w-full overflow-hidden rounded-xl border">
      <ng-map [center]="[-0.1278, 51.5074]" [zoom]="12">
        <ng-map-controls />

        <!-- No marker needed: a popup can sit at any coordinate. [(open)] keeps your state in sync. -->
        <ng-map-popup
          [longitude]="-0.1195"
          [latitude]="51.5033"
          [closeButton]="true"
          [closeOnClick]="false"
          [(open)]="open"
        >
          <div class="flex flex-col gap-1 pr-4">
            <p class="font-medium">London Eye</p>
            <p class="text-muted-foreground text-xs">135 m tall, on the south bank of the Thames.</p>
          </div>
        </ng-map-popup>
      </ng-map>

      <button z-button zType="secondary" zSize="sm" class="absolute top-3 left-3" (click)="open.set(!open())">
        {{ open() ? 'Close popup' : 'Open popup' }}
      </button>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapPopupComponent {
  protected readonly open = signal(true);
}

route

ng-map-route draws a line through coordinates. Bind [progress] and add ng-route-progress for the covered part, and ng-route-marker for a marker that rides along.
Loading map
import { ChangeDetectionStrategy, Component, DestroyRef, inject, signal } from '@angular/core';

import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucidePause, lucidePlay } from '@ng-icons/lucide';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardMapImports } from '@/shared/components/map/map.imports';
import { ZardSliderComponent } from '@/shared/components/slider/slider.component';

/** Pacific Coast Highway, San Francisco to San Simeon, as [longitude, latitude] pairs. */
const COAST: [number, number][] = [
  [-122.4194, 37.7749],
  [-122.5075, 37.6879],
  [-122.4286, 37.4636],
  [-122.3086, 37.1836],
  [-122.1236, 37.0036],
  [-122.0308, 36.9741],
  [-121.9486, 36.9631],
  [-121.8947, 36.6002],
  [-121.9276, 36.5552],
  [-121.9496, 36.4616],
  [-121.9021, 36.3015],
  [-121.8081, 36.2704],
  [-121.5946, 35.9988],
  [-121.3441, 35.7655],
  [-121.1665, 35.6852],
];

@Component({
  selector: 'z-demo-map-route',
  imports: [ZardMapImports, ZardButtonComponent, ZardSliderComponent, NgIcon],
  template: `
    <div class="flex w-full flex-col gap-3">
      <div class="h-[420px] w-full overflow-hidden rounded-xl border">
        <ng-map [center]="[-121.9, 36.45]" [zoom]="7">
          <ng-map-controls />

          <!-- The line is drawn faint; <ng-route-progress> paints the covered part on top. -->
          <ng-map-route [coordinates]="route" [width]="4" [opacity]="0.5" [progress]="progress()">
            <ng-route-progress color="#2563eb" />
            <ng-route-marker at="start"><ng-marker-content /></ng-route-marker>
            <ng-route-marker at="end"><ng-marker-content /></ng-route-marker>
            <ng-route-marker at="progress">
              <ng-marker-content>
                <div class="bg-primary border-background size-4 rounded-full border-2 shadow-lg"></div>
              </ng-marker-content>
            </ng-route-marker>
          </ng-map-route>
        </ng-map>
      </div>

      <div class="flex items-center gap-4">
        <button
          z-button
          zType="outline"
          zSize="icon"
          type="button"
          [attr.aria-label]="playing() ? 'Pause' : 'Play'"
          (click)="toggle()"
        >
          <ng-icon [name]="playing() ? 'lucidePause' : 'lucidePlay'" />
        </button>
        <z-slider
          class="flex-1"
          aria-label="Route progress"
          [zValue]="[progress() * 100]"
          (zSlideIndexChange)="setProgress($event[0] / 100)"
        />
        <span class="text-muted-foreground w-10 text-right text-sm tabular-nums">
          {{ (progress() * 100).toFixed(0) }}%
        </span>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  viewProviders: [provideIcons({ lucidePause, lucidePlay })],
})
export class ZardDemoMapRouteComponent {
  private timer: ReturnType<typeof setInterval> | undefined;

  protected readonly route = COAST;
  protected readonly progress = signal(0.35);
  protected readonly playing = signal(false);

  constructor() {
    inject(DestroyRef).onDestroy(() => this.stop());
  }

  protected setProgress(value: number): void {
    this.progress.set(Math.min(1, Math.max(0, value)));
  }

  protected toggle(): void {
    if (this.playing()) {
      this.stop();
      return;
    }

    if (this.progress() >= 1) {
      this.progress.set(0);
    }
    this.playing.set(true);
    this.timer = setInterval(() => {
      this.setProgress(this.progress() + 0.004);
      if (this.progress() >= 1) {
        this.stop();
      }
    }, 16);
  }

  private stop(): void {
    clearInterval(this.timer);
    this.timer = undefined;
    this.playing.set(false);
  }
}

arcs

ng-map-arc curves a line between two points, such as flights. Arcs that cross the antimeridian, like Sydney to San Francisco, take the short way across it. Hover one.
Loading map
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import type { MapArcDatum, MapArcEvent } from 'ng-mapcn';

import { ZardBadgeComponent } from '@/shared/components/badge/badge.component';
import { ZardMapImports } from '@/shared/components/map/map.imports';

interface Flight extends MapArcDatum {
  label: string;
  duration: string;
}

/** Extra keys on a datum travel with it: they come back in the events and are readable from paint expressions. */
const FLIGHTS: Flight[] = [
  { id: 'jfk-lhr', from: [-73.7781, 40.6413], to: [-0.4543, 51.47], label: 'New York to London', duration: '7 h' },
  { id: 'lhr-dxb', from: [-0.4543, 51.47], to: [55.3644, 25.2532], label: 'London to Dubai', duration: '7 h 10 min' },
  {
    id: 'dxb-sin',
    from: [55.3644, 25.2532],
    to: [103.9915, 1.3644],
    label: 'Dubai to Singapore',
    duration: '7 h 20 min',
  },
  {
    id: 'sin-syd',
    from: [103.9915, 1.3644],
    to: [151.1772, -33.9399],
    label: 'Singapore to Sydney',
    duration: '7 h 50 min',
  },
  // Longitudes run past 180: the arc takes the short way across the antimeridian.
  {
    id: 'syd-sfo',
    from: [151.1772, -33.9399],
    to: [-122.379, 37.6213],
    label: 'Sydney to San Francisco',
    duration: '13 h 5 min',
  },
  {
    id: 'sfo-hnd',
    from: [-122.379, 37.6213],
    to: [139.7798, 35.5494],
    label: 'San Francisco to Tokyo',
    duration: '11 h 10 min',
  },
  {
    id: 'hnd-sin',
    from: [139.7798, 35.5494],
    to: [103.9915, 1.3644],
    label: 'Tokyo to Singapore',
    duration: '7 h 40 min',
  },
  {
    id: 'gru-jfk',
    from: [-46.4731, -23.4356],
    to: [-73.7781, 40.6413],
    label: 'São Paulo to New York',
    duration: '10 h',
  },
  {
    id: 'jnb-lhr',
    from: [28.2461, -26.1392],
    to: [-0.4543, 51.47],
    label: 'Johannesburg to London',
    duration: '11 h 20 min',
  },
];

@Component({
  selector: 'z-demo-map-arcs',
  imports: [ZardMapImports, ZardBadgeComponent],
  template: `
    <div class="relative h-[420px] w-full overflow-hidden rounded-xl border">
      <ng-map [center]="[10, 20]" [zoom]="1.2">
        <ng-map-controls />
        <ng-map-arc
          [data]="flights"
          [curvature]="0.25"
          [paint]="{ 'line-color': '#6366f1', 'line-width': 2, 'line-opacity': 0.7 }"
          [hoverPaint]="{ 'line-color': '#f97316', 'line-width': 4, 'line-opacity': 1 }"
          (arcHover)="onHover($event)"
        />
      </ng-map>

      <z-badge zType="secondary" class="absolute top-3 left-3">
        @if (hovered(); as flight) {
          {{ flight.label }} · {{ flight.duration }}
        } @else {
          Hover an arc
        }
      </z-badge>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapArcsComponent {
  protected readonly flights = FLIGHTS;
  protected readonly hovered = signal<Flight | null>(null);

  protected onHover(event: MapArcEvent<Flight> | null): void {
    this.hovered.set(event?.arc ?? null);
  }
}

geojson

ng-map-geojson renders GeoJSON as fill and outline layers. With [blank] on the map it makes a choropleth. Set promoteId and fillHoverPaint for a declarative hover highlight.
Loading map
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import type { ExpressionSpecification } from 'maplibre-gl';
import type { MapGeoJSONEvent } from 'ng-mapcn';

import { ZardMapImports } from '@/shared/components/map/map.imports';

interface Country {
  ADM0_A3: string;
  NAME: string;
  POP_EST: number;
}

/** Natural Earth countries (public domain). A URL works as well as inline GeoJSON. */
const COUNTRIES =
  'https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_110m_admin_0_countries.geojson';

/** The legend: the same steps and colors as the fill expression below. */
const STEPS = [
  { from: 0, color: '#dbeafe', label: '< 10 M' },
  { from: 1e7, color: '#93c5fd', label: '10 M' },
  { from: 5e7, color: '#60a5fa', label: '50 M' },
  { from: 1e8, color: '#3b82f6', label: '100 M' },
  { from: 3e8, color: '#1d4ed8', label: '300 M+' },
];

@Component({
  selector: 'z-demo-map-geojson',
  imports: [ZardMapImports],
  template: `
    <div class="relative h-[420px] w-full overflow-hidden rounded-xl border">
      <!-- [blank] drops the basemap: the choropleth is the map. -->
      <ng-map [blank]="true" [center]="[10, 25]" [zoom]="1.4">
        <ng-map-controls />
        <ng-map-geojson
          [data]="countries"
          promoteId="ADM0_A3"
          [interactive]="true"
          [fillPaint]="fillPaint"
          [fillHoverPaint]="{ 'fill-color': '#f97316' }"
          [linePaint]="{ 'line-color': '#94a3b8', 'line-width': 0.5 }"
          (featureHover)="onHover($event)"
        />
      </ng-map>

      <div
        class="bg-popover text-popover-foreground ring-foreground/10 absolute top-3 left-3 rounded-lg px-3 py-2 text-sm shadow-md ring-1"
        aria-live="polite"
      >
        @if (hovered(); as country) {
          <span class="font-medium">{{ country.NAME }}</span>
          <span class="text-muted-foreground">· {{ (country.POP_EST / 1e6).toFixed(1) }} M people</span>
        } @else {
          <span class="text-muted-foreground">Hover a country</span>
        }
      </div>

      <ul
        class="bg-popover text-popover-foreground ring-foreground/10 absolute bottom-3 left-3 flex flex-wrap gap-x-3 gap-y-1 rounded-lg px-3 py-2 text-xs shadow-md ring-1"
        aria-label="Population legend"
      >
        @for (step of steps; track step.from) {
          <li class="flex items-center gap-1.5">
            <span class="size-3 rounded-sm" [style.background]="step.color"></span>
            {{ step.label }}
          </li>
        }
      </ul>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapGeojsonComponent {
  protected readonly countries = COUNTRIES;
  protected readonly steps = STEPS;
  protected readonly hovered = signal<Country | null>(null);

  protected readonly fillPaint: { 'fill-color': ExpressionSpecification; 'fill-opacity': number } = {
    'fill-color': [
      'step',
      ['get', 'POP_EST'],
      '#dbeafe',
      1e7,
      '#93c5fd',
      5e7,
      '#60a5fa',
      1e8,
      '#3b82f6',
      3e8,
      '#1d4ed8',
    ],
    'fill-opacity': 0.85,
  };

  protected onHover(event: MapGeoJSONEvent | null): void {
    // The data is a URL, so the compiler cannot know the properties: say what Natural Earth puts in them.
    this.hovered.set((event?.feature.properties as Country | undefined) ?? null);
  }
}

clusters

ng-map-cluster-layer groups points into clusters with counts and colors by size. Click a cluster to zoom into it, or a single point to select it. Use it, not hundreds of markers, for dense data.
Loading map
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import type { FeatureCollection, Point } from 'geojson';
import type { ClusterPointClickEvent } from 'ng-mapcn';

import { ZardBadgeComponent } from '@/shared/components/badge/badge.component';
import { ZardMapImports } from '@/shared/components/map/map.imports';

interface Store {
  id: number;
  name: string;
}

const CITIES: [string, number, number][] = [
  ['London', -0.1276, 51.5072],
  ['Paris', 2.3522, 48.8566],
  ['Berlin', 13.405, 52.52],
  ['Madrid', -3.7038, 40.4168],
  ['Rome', 12.4964, 41.9028],
  ['Amsterdam', 4.9041, 52.3676],
  ['Vienna', 16.3738, 48.2082],
  ['Warsaw', 21.0122, 52.2297],
  ['Lisbon', -9.1393, 38.7223],
  ['Stockholm', 18.0686, 59.3293],
];

/** A small seeded generator, so the demo draws the same stores on every load. */
function random(seed: number): () => number {
  return () => {
    seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function createStores(count: number): FeatureCollection<Point, Store> {
  const next = random(7);

  return {
    type: 'FeatureCollection',
    features: Array.from({ length: count }, (_, id) => {
      const [city, lng, lat] = CITIES[id % CITIES.length];
      // Most stores sit in the center, a few far out: that is what makes the clusters read well.
      const spread = 0.35 * next() ** 2;
      const angle = next() * Math.PI * 2;

      return {
        type: 'Feature',
        properties: { id, name: `${city} store ${id + 1}` },
        geometry: {
          type: 'Point',
          coordinates: [lng + Math.cos(angle) * spread * 1.5, lat + Math.sin(angle) * spread],
        },
      };
    }),
  };
}

@Component({
  selector: 'z-demo-map-clusters',
  imports: [ZardMapImports, ZardBadgeComponent],
  template: `
    <div class="relative h-[420px] w-full overflow-hidden rounded-xl border">
      <ng-map [center]="[8, 48]" [zoom]="3.6">
        <ng-map-controls />
        <!-- Click a cluster to zoom into it. Click a single point to select it. -->
        <ng-map-cluster-layer
          [data]="stores"
          [clusterColors]="['#60a5fa', '#3b82f6', '#1d4ed8']"
          [clusterThresholds]="[25, 100]"
          pointColor="#f97316"
          (pointClick)="onPointClick($event)"
        />
      </ng-map>

      <z-badge zType="secondary" class="absolute top-3 left-3">
        {{ selected() ?? stores.features.length + ' stores, click a cluster to zoom in' }}
      </z-badge>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapClustersComponent {
  protected readonly stores = createStores(600);
  protected readonly selected = signal<string | null>(null);

  protected onPointClick(event: ClusterPointClickEvent<Store>): void {
    this.selected.set(event.feature.properties.name);
  }
}

heatmap

ng-map-heatmap turns points into a density map. Give it a URL and it fetches the GeoJSON; weightProperty makes some points count more than others.
Loading map
import { ChangeDetectionStrategy, Component } from '@angular/core';

import type { ExpressionSpecification } from 'maplibre-gl';

import { ZardBadgeComponent } from '@/shared/components/badge/badge.component';
import { ZardMapImports } from '@/shared/components/map/map.imports';

/** Every earthquake of magnitude 2.5 or more in the last 30 days, from the USGS (public domain). No key needed. */
const EARTHQUAKES = 'https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_month.geojson';

@Component({
  selector: 'z-demo-map-heatmap',
  imports: [ZardMapImports, ZardBadgeComponent],
  template: `
    <div class="relative h-[420px] w-full overflow-hidden rounded-xl border">
      <ng-map [center]="[150, 10]" [zoom]="1.6">
        <ng-map-controls />
        <!-- A URL is fetched by the map. The weight of each point comes from its magnitude. -->
        <ng-map-heatmap
          [data]="earthquakes"
          weightProperty="mag"
          [weightRange]="[2.5, 7]"
          [radius]="radius"
          [intensity]="1.4"
          [opacity]="0.9"
        />
      </ng-map>

      <z-badge zType="secondary" class="absolute top-3 left-3">Earthquakes in the last 30 days · USGS</z-badge>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapHeatmapComponent {
  protected readonly earthquakes = EARTHQUAKES;

  /** The radius grows with the zoom, so the heat keeps its shape as you move in. */
  protected readonly radius: ExpressionSpecification = ['interpolate', ['linear'], ['zoom'], 0, 8, 4, 22, 8, 40];
}

tile sources

ng-map-raster-source adds XYZ raster tiles on top of the basemap. ng-map-vector-source adds vector tiles (MVT, TileJSON or PMTiles), and a layer component such as ng-map-fill-extrusion draws them.
Loading map
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import { ZardMapImports } from '@/shared/components/map/map.imports';
import { ZardSliderComponent } from '@/shared/components/slider/slider.component';
import {
  ZardToggleGroupComponent,
  type ZardToggleGroupItem,
} from '@/shared/components/toggle-group/toggle-group.component';

type TileSource = 'satellite' | 'topo' | 'buildings';

@Component({
  selector: 'z-demo-map-tile-sources',
  imports: [ZardMapImports, ZardToggleGroupComponent, ZardSliderComponent],
  template: `
    <div class="flex w-full flex-col gap-3">
      <div class="flex flex-wrap items-center gap-4">
        <z-toggle-group
          zMode="single"
          zType="outline"
          zSize="sm"
          [zItems]="sources"
          [zDefaultValue]="source()"
          (valueChange)="onSourceChange($event)"
        />
        @if (source() !== 'buildings') {
          <z-slider
            class="w-40"
            aria-label="Layer opacity"
            [zValue]="[opacity() * 100]"
            (zSlideIndexChange)="opacity.set($event[0] / 100)"
          />
        }
      </div>

      <div class="h-[420px] w-full overflow-hidden rounded-xl border">
        <ng-map [center]="[-74.0134, 40.7061]" [zoom]="14.2" [options]="{ pitch: 50, bearing: -17 }">
          <ng-map-controls />

          <!-- Raster tiles: XYZ URL templates, drawn above the basemap. -->
          @switch (source()) {
            @case ('satellite') {
              <ng-map-raster-source
                id="imagery"
                [tiles]="[
                  'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
                ]"
                [tileSize]="256"
                [maxzoom]="19"
                attribution="Tiles © Esri"
                [opacity]="opacity()"
              />
            }
            @case ('topo') {
              <ng-map-raster-source
                id="topo"
                [tiles]="[
                  'https://a.tile.opentopomap.org/{z}/{x}/{y}.png',
                  'https://b.tile.opentopomap.org/{z}/{x}/{y}.png',
                  'https://c.tile.opentopomap.org/{z}/{x}/{y}.png',
                ]"
                [tileSize]="256"
                [maxzoom]="17"
                attribution="© OpenStreetMap contributors, SRTM · © OpenTopoMap (CC-BY-SA)"
                [opacity]="opacity()"
              />
            }
            @case ('buildings') {
              <!-- Vector tiles: the source only adds the data. A layer decides how to draw it. -->
              <ng-map-vector-source
                id="openfreemap"
                url="https://tiles.openfreemap.org/planet"
                attribution="© OpenFreeMap © OpenMapTiles"
              />
              <ng-map-fill-extrusion
                source="openfreemap"
                sourceLayer="building"
                heightProperty="render_height"
                baseProperty="render_min_height"
                color="#6366f1"
                [opacity]="0.75"
                [minZoom]="13"
              />
            }
          }
        </ng-map>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapTileSourcesComponent {
  protected readonly source = signal<TileSource>('satellite');
  protected readonly opacity = signal(0.85);

  protected readonly sources: ZardToggleGroupItem[] = [
    { value: 'satellite', label: 'Satellite (raster)' },
    { value: 'topo', label: 'Topographic (raster)' },
    { value: 'buildings', label: 'Buildings (vector)' },
  ];

  protected onSourceChange(value: string | string[]): void {
    if (typeof value === 'string' && value) {
      this.source.set(value as TileSource);
    }
  }
}

3d

Tilt the map (hold right-click and drag, or Ctrl and drag) to see ng-map-fill-extrusion lift the buildings and ng-map-terrain raise the relief. Extrusions follow the picked basemap; a raster one (OSM) has no buildings.
Loading map
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import type { Map as MapLibreMap } from 'maplibre-gl';

import { ZardMapImports } from '@/shared/components/map/map.imports';
import { ZardSwitchComponent } from '@/shared/components/switch/switch.component';
import {
  ZardToggleGroupComponent,
  type ZardToggleGroupItem,
} from '@/shared/components/toggle-group/toggle-group.component';

type Place = 'manhattan' | 'matterhorn';

const CAMERAS: Record<Place, { center: [number, number]; zoom: number; pitch: number; bearing: number }> = {
  manhattan: { center: [-74.0134, 40.7061], zoom: 15.6, pitch: 62, bearing: -20 },
  matterhorn: { center: [7.6586, 45.9763], zoom: 11.6, pitch: 72, bearing: 25 },
};

/** OpenMapTiles basemaps name their vector source differently from Carto. */
const OPENMAPTILES_SOURCES = ['openmaptiles', 'maptiler_planet'];

@Component({
  selector: 'z-demo-map-three-d',
  imports: [ZardMapImports, ZardToggleGroupComponent, ZardSwitchComponent],
  template: `
    <div class="flex w-full flex-col gap-3">
      <div class="flex flex-wrap items-center gap-4">
        <z-toggle-group
          zMode="single"
          zType="outline"
          zSize="sm"
          [zItems]="places"
          [zDefaultValue]="place()"
          (valueChange)="onPlaceChange($event)"
        />
        <z-switch [(zChecked)]="terrain">Terrain</z-switch>
      </div>

      <div class="h-[420px] w-full overflow-hidden rounded-xl border">
        <ng-map
          [center]="camera.center"
          [zoom]="camera.zoom"
          [options]="{ pitch: camera.pitch, bearing: camera.bearing }"
          (mapReady)="onMapReady($event)"
        >
          <ng-map-controls [showCompass]="true" />
          <!-- Buildings appear from zoom 14, with the heights the basemap carries. -->
          <ng-map-fill-extrusion [source]="buildings()" [sourceLayer]="buildings() ? 'building' : undefined" />
          @if (terrain()) {
            <!-- Elevation from public Terrarium tiles, plus a hillshade so the relief reads. -->
            <ng-map-terrain [exaggeration]="1.5" [hillshade]="true" />
          }
        </ng-map>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapThreeDComponent {
  /** The buildings' source when the basemap is OpenMapTiles; Carto is detected on its own. */
  protected readonly buildings = signal<string | undefined>(undefined);
  protected readonly camera = CAMERAS.manhattan;
  protected map: MapLibreMap | null = null;

  protected readonly place = signal<Place>('manhattan');
  protected readonly terrain = signal(true);

  protected readonly places: ZardToggleGroupItem[] = [
    { value: 'manhattan', label: 'Manhattan (buildings)' },
    { value: 'matterhorn', label: 'Matterhorn (terrain)' },
  ];

  protected onMapReady(map: MapLibreMap): void {
    this.map = map;
    const sync = () => this.buildings.set(OPENMAPTILES_SOURCES.find(id => map.getSource(id)));
    sync();
    map.on('styledata', sync);
  }

  protected onPlaceChange(value: string | string[]): void {
    if (typeof value !== 'string' || !value) {
      return;
    }

    this.place.set(value as Place);
    this.map?.flyTo({ ...CAMERAS[value as Place], duration: 3500, essential: true });
  }
}

custom styles

[styles] takes a style URL or a full MapLibre style JSON for each theme. Define the objects once, outside the component: the map compares styles by identity before it restyles.
Loading map
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';

import type { StyleSpecification } from 'maplibre-gl';

import { ZardMapImports } from '@/shared/components/map/map.imports';
import {
  ZardToggleGroupComponent,
  type ZardToggleGroupItem,
} from '@/shared/components/toggle-group/toggle-group.component';

const COUNTRIES =
  'https://cdn.jsdelivr.net/gh/nvkelso/natural-earth-vector@master/geojson/ne_110m_admin_0_countries.geojson';

/** A MapLibre style is plain JSON: sources say where the data is, layers say how to draw it. */
function blueprint(colors: { sea: string; land: string; line: string }): StyleSpecification {
  return {
    version: 8,
    sources: { countries: { type: 'geojson', data: COUNTRIES, attribution: 'Natural Earth' } },
    layers: [
      { id: 'sea', type: 'background', paint: { 'background-color': colors.sea } },
      { id: 'land', type: 'fill', source: 'countries', paint: { 'fill-color': colors.land } },
      {
        id: 'borders',
        type: 'line',
        source: 'countries',
        paint: { 'line-color': colors.line, 'line-width': 0.8, 'line-dasharray': [2, 1] },
      },
    ],
  };
}

/** Define styles once, outside the component: `<ng-map>` compares them by identity before it restyles. */
const BLUEPRINT = {
  light: blueprint({ sea: '#eff6ff', land: '#bfdbfe', line: '#2563eb' }),
  dark: blueprint({ sea: '#0b1220', land: '#14213d', line: '#3b82f6' }),
};

const LIBERTY = {
  light: 'https://tiles.openfreemap.org/styles/liberty',
  dark: 'https://tiles.openfreemap.org/styles/dark',
};

@Component({
  selector: 'z-demo-map-custom-styles',
  imports: [ZardMapImports, ZardToggleGroupComponent],
  template: `
    <div class="flex w-full flex-col gap-3">
      <z-toggle-group
        zMode="single"
        zType="outline"
        zSize="sm"
        [zItems]="options"
        [zDefaultValue]="choice()"
        (valueChange)="onChoiceChange($event)"
      />

      <div class="h-[420px] w-full overflow-hidden rounded-xl border">
        <!-- Light and dark are separate styles: the map picks one from the theme and swaps live. -->
        <ng-map [styles]="styles()" [center]="[12, 38]" [zoom]="2.4">
          <ng-map-controls />
        </ng-map>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapCustomStylesComponent {
  protected readonly choice = signal<'liberty' | 'blueprint'>('blueprint');
  protected readonly styles = computed(() => (this.choice() === 'blueprint' ? BLUEPRINT : LIBERTY));

  protected readonly options: ZardToggleGroupItem[] = [
    { value: 'blueprint', label: 'Blueprint (style JSON)' },
    { value: 'liberty', label: 'Liberty (style URL)' },
  ];

  protected onChoiceChange(value: string | string[]): void {
    if (value === 'blueprint' || value === 'liberty') {
      this.choice.set(value);
    }
  }
}

viewport

Bind [viewport] and (viewportChange) to own the camera: the readout follows every frame while you pan, zoom, rotate and tilt, and the buttons move the map.
Loading map
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

import type { MapViewport } from 'ng-mapcn';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardMapImports } from '@/shared/components/map/map.imports';

const PLACES: { name: string; viewport: MapViewport }[] = [
  { name: 'Tokyo', viewport: { center: [139.6917, 35.6895], zoom: 10, bearing: 0, pitch: 0 } },
  { name: 'New York', viewport: { center: [-74.006, 40.7128], zoom: 11, bearing: 29, pitch: 45 } },
  { name: 'Cape Town', viewport: { center: [18.4241, -33.9249], zoom: 10.5, bearing: 0, pitch: 0 } },
];

@Component({
  selector: 'z-demo-map-viewport',
  imports: [ZardMapImports, ZardButtonComponent],
  template: `
    <div class="flex w-full flex-col gap-3">
      <div class="flex flex-wrap items-center gap-2">
        @for (place of places; track place.name) {
          <button z-button zType="outline" zSize="sm" type="button" (click)="viewport.set(place.viewport)">
            {{ place.name }}
          </button>
        }
      </div>

      <div class="relative h-[420px] w-full overflow-hidden rounded-xl border">
        <!-- The map reports every frame, you hand the state back: the viewport is yours. -->
        <ng-map [viewport]="viewport()" (viewportChange)="viewport.set($event)">
          <ng-map-controls [showCompass]="true" />
        </ng-map>

        <dl
          class="bg-popover text-popover-foreground ring-foreground/10 absolute top-3 left-3 grid grid-cols-[auto_auto] gap-x-3 gap-y-0.5 rounded-lg px-3 py-2 font-mono text-xs shadow-md ring-1"
          aria-live="off"
        >
          <dt class="text-muted-foreground">lng</dt>
          <dd class="text-right tabular-nums">{{ viewport().center[0].toFixed(4) }}</dd>
          <dt class="text-muted-foreground">lat</dt>
          <dd class="text-right tabular-nums">{{ viewport().center[1].toFixed(4) }}</dd>
          <dt class="text-muted-foreground">zoom</dt>
          <dd class="text-right tabular-nums">{{ viewport().zoom.toFixed(2) }}</dd>
          <dt class="text-muted-foreground">bearing</dt>
          <dd class="text-right tabular-nums">{{ viewport().bearing.toFixed(0) }}°</dd>
          <dt class="text-muted-foreground">pitch</dt>
          <dd class="text-right tabular-nums">{{ viewport().pitch.toFixed(0) }}°</dd>
        </dl>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapViewportComponent {
  protected readonly places = PLACES;
  protected readonly viewport = signal<MapViewport>(PLACES[0].viewport);
}

draw

ng-map-draw draws, selects and edits points, lines and polygons. It comes from the optional map-draw item (npx zard-cli add map-draw), which adds terra-draw; the core map never loads it. Pick a tool, click the map, and double-click or press Enter to finish.
Loading map
import { ChangeDetectionStrategy, Component, computed, signal, viewChild } from '@angular/core';

import type { MapDrawFeature, MapDrawFeatureCollection, MapDrawMode } from 'ng-mapcn/draw';

import { ZardBadgeComponent } from '@/shared/components/badge/badge.component';
import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardMapDrawImports } from '@/shared/components/map-draw/map-draw.imports';
import {
  ZardToggleGroupComponent,
  type ZardToggleGroupItem,
} from '@/shared/components/toggle-group/toggle-group.component';

const EARTH_RADIUS_M = 6_371_008.8;
const toRad = (degrees: number): number => (degrees * Math.PI) / 180;

/** Great-circle distance in meters (haversine). */
function distance([lng1, lat1]: number[], [lng2, lat2]: number[]): number {
  const a =
    Math.sin(toRad(lat2 - lat1) / 2) ** 2 +
    Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(toRad(lng2 - lng1) / 2) ** 2;
  return 2 * EARTH_RADIUS_M * Math.asin(Math.sqrt(a));
}

function formatArea(km2: number): string {
  return km2 < 0.01 ? `${Math.round(km2 * 1e6).toLocaleString('en-US')} m²` : `${km2.toFixed(2)} km²`;
}

/** Area of a polygon ring on the sphere, in square meters. */
function ringArea(ring: number[][]): number {
  let sum = 0;
  for (let i = 0; i < ring.length - 1; i++) {
    const [lng1, lat1] = ring[i];
    const [lng2, lat2] = ring[i + 1];
    sum += toRad(lng2 - lng1) * (2 + Math.sin(toRad(lat1)) + Math.sin(toRad(lat2)));
  }
  return Math.abs((sum * EARTH_RADIUS_M ** 2) / 2);
}

function measure(feature: MapDrawFeature): string {
  const { geometry } = feature;

  switch (geometry.type) {
    case 'Point':
      return `${geometry.coordinates[1].toFixed(4)}, ${geometry.coordinates[0].toFixed(4)}`;
    case 'LineString': {
      const meters = geometry.coordinates
        .slice(1)
        .reduce((sum, point, i) => sum + distance(geometry.coordinates[i], point), 0);
      return meters < 1000 ? `${meters.toFixed(0)} m` : `${(meters / 1000).toFixed(2)} km`;
    }
    case 'Polygon': {
      const km2 = ringArea(geometry.coordinates[0]) / 1e6;
      return formatArea(km2);
    }
  }
}

@Component({
  selector: 'z-demo-map-draw',
  imports: [ZardMapDrawImports, ZardToggleGroupComponent, ZardButtonComponent, ZardBadgeComponent],
  template: `
    <div class="flex w-full flex-col gap-3">
      <div class="flex flex-wrap items-center gap-2">
        <z-toggle-group
          zMode="single"
          zType="outline"
          zSize="sm"
          [zItems]="modes"
          [zValue]="mode()"
          (valueChange)="onModeChange($event)"
        />
        <button
          z-button
          zType="ghost"
          zSize="sm"
          type="button"
          [disabled]="!features().features.length"
          (click)="clear()"
        >
          Clear
        </button>
      </div>

      <div class="h-[420px] w-full overflow-hidden rounded-xl border">
        <ng-map [center]="[-73.9654, 40.7829]" [zoom]="12.8">
          <ng-map-controls />
          <!-- Two-way: [(features)] is a GeoJSON FeatureCollection, [(mode)] the active tool. -->
          <ng-map-draw
            [(mode)]="mode"
            [(features)]="features"
            [modes]="['point', 'line', 'polygon']"
            [editable]="true"
          />
        </ng-map>
      </div>

      <ul class="flex min-h-7 flex-wrap gap-2" aria-label="Drawn features" aria-live="polite">
        @for (feature of readout(); track feature.id) {
          <li>
            <z-badge zType="secondary" class="gap-1.5">
              <span class="font-medium capitalize">{{ feature.kind }}</span>
              <span class="tabular-nums">{{ feature.value }}</span>
            </z-badge>
          </li>
        } @empty {
          <li class="text-muted-foreground text-sm">
            Pick a tool, then click the map. Double-click or press Enter to finish.
          </li>
        }
      </ul>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: { class: 'block w-full' },
})
export class ZardDemoMapDrawComponent {
  private readonly toolbar = viewChild.required(ZardToggleGroupComponent);

  protected readonly mode = signal<MapDrawMode>('polygon');
  protected readonly features = signal<MapDrawFeatureCollection>({ type: 'FeatureCollection', features: [] });

  protected readonly readout = computed(() =>
    this.features().features.map(feature => ({
      id: feature.id,
      kind: feature.geometry.type === 'LineString' ? 'line' : feature.geometry.type.toLowerCase(),
      value: measure(feature as MapDrawFeature),
    })),
  );

  protected readonly modes: ZardToggleGroupItem[] = [
    { value: 'static', label: 'View' },
    { value: 'select', label: 'Select' },
    { value: 'point', label: 'Point' },
    { value: 'line', label: 'Line' },
    { value: 'polygon', label: 'Polygon' },
  ];

  /** Re-clicking the active tool unpresses it while it keeps drawing, so press it back. */
  protected onModeChange(value: string | string[]): void {
    if (typeof value === 'string' && value) {
      this.mode.set(value as MapDrawMode);
    } else {
      this.toolbar().writeValue(this.mode());
    }
  }

  protected clear(): void {
    this.features.set({ type: 'FeatureCollection', features: [] });
  }
}

lazy loading

MapLibre is heavy and ng-mapcn is not tree-shakeable, so keep it out of the initial bundle: render maps from a lazy route, or inside @defer. For the same reason, import provideZardMap() only from a routes file loaded with loadChildren: importing it in app.routes.ts or app.config.ts puts MapLibre in the initial bundle.
map.page.ts
import { Component } from '@angular/core';

import { ZardMapImports } from '@/shared/components/map/map.imports';

// MapLibre is heavy: keep it out of the initial bundle with a lazy route (loadComponent), or with @defer when the
// map is only part of a page.
@Component({
  selector: 'app-map-page',
  imports: [ZardMapImports],
  template: `
    @defer (on viewport) {
      <div class="h-96">
        <ng-map [center]="[2.35, 48.85]" [zoom]="10" />
      </div>
    } @placeholder {
      <div class="bg-muted h-96 animate-pulse rounded-xl"></div>
    }
  `,
})
export class MapPage {}
Copy

webgl2

MapLibre 6 needs WebGL2. Without it the map cannot start, so check first and show a message instead of an empty box. (mapError) also reports tile and style failures, so it is not the right signal for this.
map.page.ts
import { afterNextRender, Component, signal } from '@angular/core';

import { ZardMapImports } from '@/shared/components/map/map.imports';

// MapLibre 6 needs WebGL2. Without it the map cannot start, so check first and show something useful instead of an
// empty box. (mapError) also reports tile and style failures, so do not use it as the "no WebGL2" signal.
@Component({
  selector: 'app-map-page',
  imports: [ZardMapImports],
  template: `
    <div class="h-96">
      @if (supported()) {
        <ng-map [center]="[2.35, 48.85]" [zoom]="10" (mapError)="onError($event)" />
      } @else {
        <p class="text-muted-foreground p-6 text-sm">Maps need WebGL2, which this browser does not provide.</p>
      }
    </div>
  `,
})
export class MapPage {
  protected readonly supported = signal(true);

  constructor() {
    afterNextRender(() => this.supported.set(!!document.createElement('canvas').getContext('webgl2')));
  }

  protected onError(error: Error): void {
    console.warn('Map error', error);
  }
}
Copy

Map styles

Not every style works in dark mode
Carto Voyager, OpenFreeMap Liberty, OpenFreeMap Bright, OpenFreeMap Positron, OpenStreetMap Standard, Stadia Maps Outdoors, Stadia Maps OSM Bright are drawn for light mode only: in dark mode they stay light or fall back to a shared dark style, so they all look alike. OpenFreeMap Dark, OpenFreeMap Fiord are dark only. The picker marks them Light only and Dark only. If your app has a dark mode, pick a preset with its own dark variant.
Maps without [styles] share one app-wide basemap that switches live. Precedence: [styles], [blank], the app-wide basemap, then Carto.
  • Presets pair a light and a dark style, except the single-theme ones above.
  • z-map-style-select is the picker; ZardMapStyleService holds its state.
  • Key-based presets take their key from provideZardMap({ keys }) or the picker.
PresetProviderKeyLight / darkAttributionUsage
carto-positron Carto NonePositron / Dark Matter© OpenStreetMap contributors © CARTOFree for non-commercial and low-traffic use. Commercial products need a Carto plan.
carto-voyager Carto NoneVoyager (light only, Dark Matter otherwise)© OpenStreetMap contributors © CARTOFree for non-commercial and low-traffic use. Commercial products need a Carto plan.
openfreemap-liberty OpenFreeMap NoneLiberty (light only, Dark otherwise)© OpenFreeMap © OpenMapTiles Data from OpenStreetMapFree with no registration and no hard limit. Donations keep it running.
openfreemap-bright OpenFreeMap NoneBright (light only, Dark otherwise)© OpenFreeMap © OpenMapTiles Data from OpenStreetMapFree with no registration and no hard limit. Donations keep it running.
openfreemap-positron OpenFreeMap NonePositron (light only, Dark otherwise)© OpenFreeMap © OpenMapTiles Data from OpenStreetMapFree with no registration and no hard limit. Donations keep it running.
openfreemap-dark OpenFreeMap NoneDark (dark only)© OpenFreeMap © OpenMapTiles Data from OpenStreetMapFree with no registration and no hard limit. Donations keep it running.
openfreemap-fiord OpenFreeMap NoneFiord (dark only)© OpenFreeMap © OpenMapTiles Data from OpenStreetMapFree with no registration and no hard limit. Donations keep it running.
osm-raster OpenStreetMap NoneStandard (light only, Standard, dimmed otherwise)© OpenStreetMap contributorsDevelopment only
maptiler-streets MapTiler RequiredStreets / Streets Dark© MapTiler © OpenStreetMap contributorsFree plan for non-commercial use with a monthly request quota. Restrict the key to your domain.
maptiler-dataviz MapTiler RequiredDataviz / Dataviz Dark© MapTiler © OpenStreetMap contributorsFree plan for non-commercial use with a monthly request quota. Restrict the key to your domain.
maptiler-satellite MapTiler RequiredHybrid / Hybrid© MapTiler © OpenStreetMap contributorsFree plan for non-commercial use with a monthly request quota. Restrict the key to your domain.
maptiler-outdoor MapTiler RequiredOutdoor / Outdoor Dark© MapTiler © OpenStreetMap contributorsFree plan for non-commercial use with a monthly request quota. Restrict the key to your domain.
stadia-alidade-smooth Stadia Maps RequiredAlidade Smooth / Alidade Smooth Dark© Stadia Maps © OpenMapTiles © OpenStreetMap contributorsWorks without a key on localhost. Production domains need a (free) account and a key.
stadia-outdoors Stadia Maps RequiredOutdoors (light only)© Stadia Maps © OpenMapTiles © OpenStreetMap contributorsWorks without a key on localhost. Production domains need a (free) account and a key.
stadia-osm-bright Stadia Maps RequiredOSM Bright (light only)© Stadia Maps © OpenMapTiles © OpenStreetMap contributorsWorks without a key on localhost. Production domains need a (free) account and a key.

style picker

Place z-map-style-select anywhere. Key-based presets ask for their key inline.

Register

map.routes.ts Copy
import { provideZardMap } from '@/shared/components/map/map.provider';

// A routes file of its own, loaded from app.routes.ts with loadChildren, keeps MapLibre out of the initial bundle:
//   { path: 'map', loadChildren: () => import('./map.routes').then(m => m.MAP_ROUTES) }
export const MAP_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () => import('./map.page').then(m => m.MapPage),
    providers: [
      provideZardMap({
        // The preset every map starts with (see the table below). Carto Positron / Dark Matter by default.
        style: 'openfreemap-liberty',
        // Key-based presets only unlock with a key. Never commit a real one: read it from your environment.
        keys: { maptiler: environment.maptilerKey },
        // Remember the choice in localStorage (browser only), so it survives reloads and page changes.
        persist: true,
      }),
    ],
  },
];
Loading map
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardMapImports } from '@/shared/components/map/map.imports';
import { ZardMapStyleSelectComponent } from '@/shared/components/map-style-select/map-style-select.component';

@Component({
  selector: 'z-demo-map-style-picker',
  imports: [ZardMapImports, ZardMapStyleSelectComponent],
  template: `
    <div class="relative h-[420px] w-full overflow-hidden rounded-xl border">
      <!-- No [styles] here: this map follows the app-wide basemap that provideZardMap() registers. -->
      <ng-map [center]="[-0.1276, 51.5072]" [zoom]="11.5">
        <ng-map-controls />
        <ng-map-marker [longitude]="-0.1276" [latitude]="51.5072" ariaLabel="London">
          <ng-marker-content />
        </ng-map-marker>
      </ng-map>

      <!-- The picker is plain HTML, so it can sit over a corner of the map, in a toolbar or in your app header. -->
      <z-map-style-select class="absolute top-3 left-3" />
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapStylePickerComponent {}

Without the picker

The service is plain signals, so any UI can switch the basemap.
basemap-toggle.ts Copy
import { Component, inject } from '@angular/core';

import { ZardMapStyleService } from '@/shared/components/map/map-style.service';

@Component({
  selector: 'app-basemap-toggle',
  template: `
    <button (click)="service.select('carto-voyager')">Voyager</button>
    <button (click)="service.select('openfreemap-liberty')">Liberty</button>
    <p>Now showing {{ service.preset().label }}</p>
  `,
})
export class BasemapToggle {
  // Every <ng-map> without its own [styles] swaps live: no reload, no remount.
  protected readonly service = inject(ZardMapStyleService);
}

style override

A map with its own [styles] ignores the picker.
Loading map
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardBadgeComponent } from '@/shared/components/badge/badge.component';
import { ZardMapImports } from '@/shared/components/map/map.imports';
import { ZardMapStyleSelectComponent } from '@/shared/components/map-style-select/map-style-select.component';

/** Defined once, outside the component: `<ng-map>` compares styles by identity before it restyles. */
const FIORD = {
  light: 'https://tiles.openfreemap.org/styles/fiord',
  dark: 'https://tiles.openfreemap.org/styles/fiord',
};

@Component({
  selector: 'z-demo-map-style-override',
  imports: [ZardMapImports, ZardMapStyleSelectComponent, ZardBadgeComponent],
  template: `
    <div class="flex w-full flex-col gap-3">
      <z-map-style-select class="max-w-60" />

      <div class="grid gap-4 md:grid-cols-2">
        <div class="relative h-[320px] overflow-hidden rounded-xl border">
          <ng-map [center]="[4.9041, 52.3676]" [zoom]="11" />
          <z-badge zType="secondary" class="absolute top-3 left-3">Follows the picker</z-badge>
        </div>

        <div class="relative h-[320px] overflow-hidden rounded-xl border">
          <!-- A map with its own [styles] ignores the picker. -->
          <ng-map [styles]="fiord" [center]="[4.9041, 52.3676]" [zoom]="11" />
          <z-badge zType="secondary" class="absolute top-3 left-3">Own [styles]: Fiord</z-badge>
        </div>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapStyleOverrideComponent {
  protected readonly fiord = FIORD;
}

style blank

[blank] replaces the basemap with an empty, transparent style.
Loading map
import { ChangeDetectionStrategy, Component } from '@angular/core';

import type { Feature, Polygon } from 'geojson';

import { ZardMapImports } from '@/shared/components/map/map.imports';

/** A delivery zone around the Eiffel Tower. With [blank] there is no basemap behind it. */
const ZONE: Feature<Polygon> = {
  type: 'Feature',
  properties: {},
  geometry: {
    type: 'Polygon',
    coordinates: [
      [
        [2.2945, 48.8584],
        [2.31, 48.8641],
        [2.3262, 48.8606],
        [2.3301, 48.8512],
        [2.3128, 48.8449],
        [2.2965, 48.8478],
        [2.2945, 48.8584],
      ],
    ],
  },
};

@Component({
  selector: 'z-demo-map-style-blank',
  imports: [ZardMapImports],
  template: `
    <!-- [blank] swaps the basemap for an empty, transparent style: whatever is behind the map shows through. -->
    <div class="bg-muted/40 h-[320px] w-full overflow-hidden rounded-xl border">
      <ng-map [blank]="true" [center]="[2.3125, 48.8546]" [zoom]="12.6">
        <ng-map-geojson
          [data]="zone"
          [fillPaint]="{ 'fill-color': '#6366f1', 'fill-opacity': 0.25 }"
          [linePaint]="{ 'line-color': '#6366f1', 'line-width': 2 }"
        />
        <ng-map-marker [longitude]="2.2945" [latitude]="48.8584" ariaLabel="Eiffel Tower">
          <ng-marker-content />
          <ng-marker-tooltip>Eiffel Tower</ng-marker-tooltip>
        </ng-map-marker>
      </ng-map>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMapStyleBlankComponent {
  protected readonly zone = ZONE;
}

API Reference

ng-map

The map host. It fills its parent, so give the parent an explicit height.

Prop
[center]

Initial [longitude, latitude]

Type
[number, number]
Default
[0, 0]
[zoom]

Initial zoom level

Type
number
Default
2
[viewport]

Controlled camera; pair with (viewportChange)

Type
Partial<MapViewport>
Default
—
[styles]

Light and dark basemap for this map only

Type
MapStylesConfig
Default
app-wide basemap
[blank]

Transparent, tile-less style. Ignored with [styles]

Type
boolean
Default
false
[theme]

Leave on auto to follow the dark class

Type
'light' | 'dark' | 'auto'
Default
'auto'
[projection]

MapLibre projection, e.g. { type: "globe" }

Type
ProjectionSpecification
Default
—
[fitBounds]

Fits the camera to bounds or the children data

Type
MapFitBoundsOptions
Default
—
[loading]

Shows the loader over the map

Type
boolean
Default
false
[labels]

Label overrides for this map

Type
Partial<MapLabels>
Default
—
[options]

Raw MapLibre MapOptions

Type
Omit<MapOptions, "container" | "style">
Default
—
Event
(mapReady)

Emits the loaded MapLibre map

Type
Map
Default
—
(viewportChange)

Emits on every camera frame

Type
MapViewport
Default
—
(mapClick)

Emits when the map is clicked

Type
MapMouseEvent
Default
—
(mapError)

Emits on a MapLibre error (WebGL2, style, tile, layer)

Type
Error
Default
—

ng-map-controls

Zoom, compass, locate and fullscreen buttons.

Prop
[position]

Corner of the map

Type
'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
Default
'bottom-right'
[showZoom]

Shows the zoom buttons

Type
boolean
Default
true
[showCompass]

Shows the compass (resets north and pitch)

Type
boolean
Default
false
[showLocate]

Shows the geolocation button

Type
boolean
Default
false
[showFullscreen]

Shows the fullscreen button

Type
boolean
Default
false
[labels]

Label overrides for the button tooltips

Type
Partial<MapLabels>
Default
—
[className]

Extra classes for the controls

Type
string
Default
—
Event
(locateChange)

Emits the located coordinates

Type
{ longitude: number; latitude: number }
Default
—
(locateError)

Emits when geolocation fails or is unavailable

Type
Error | GeolocationPositionError
Default
—

ng-map-marker

A marker with optional content, label, popup and tooltip children.

Prop
[longitude]

Longitude

Type
number
Default
—
[latitude]

Latitude

Type
number
Default
—
[draggable]

Lets the user drag the marker

Type
boolean
Default
false
[offset]

Pixel offset from the position

Type
PointLike
Default
—
[rotation]

Rotation in degrees

Type
number
Default
0
[anchor]

Part of the marker on the position (creation only)

Type
PositionAnchor
Default
'center'
[rotationAlignment]

What the rotation is relative to

Type
Alignment
Default
'auto'
[pitchAlignment]

What the pitch is relative to

Type
Alignment
Default
'auto'
[ariaLabel]

Accessible name of the marker

Type
string
Default
—
[labels]

Label overrides for the marker

Type
Partial<MapLabels>
Default
—
[keyboardStep]

Pixels moved per arrow key when dragging

Type
number
Default
10
[keyboardStepLarge]

Pixels moved with Shift held

Type
number
Default
50
Event
(markerClick)

Emits on click, Enter or Space

Type
MouseEvent
Default
—
(markerMouseEnter)

Emits when the pointer enters

Type
MouseEvent
Default
—
(markerMouseLeave)

Emits when the pointer leaves

Type
MouseEvent
Default
—
(dragStart)

Emits when a drag starts

Type
{ lng: number; lat: number }
Default
—
(drag)

Emits on every drag frame

Type
{ lng: number; lat: number }
Default
—
(dragEnd)

Emits the new position after a drag

Type
{ lng: number; lat: number }
Default
—

ng-marker-content

Custom marker HTML; leave it empty for the default dot.

Prop
[className]

Extra classes for the marker content

Type
string
Default
—

ng-marker-label

A text label above or below the marker.

Prop
[position]

Side of the marker

Type
'top' | 'bottom'
Default
'top'

ng-marker-popup

A popup that opens when the marker is clicked.

Prop
[closeButton]

Shows a close button

Type
boolean
Default
false
[offset]

Pixel offset from the marker

Type
Offset
Default
—
[anchor]

Side the popup anchors to (creation only)

Type
PositionAnchor
Default
auto
[maxWidth]

CSS max-width of the popup

Type
string
Default
'none'
[closeOnClick]

Closes on map click (creation only)

Type
boolean
Default
false
[labels]

Label overrides for the close button

Type
Partial<MapLabels>
Default
—
[className]

Extra classes for the popup card

Type
string
Default
—
Event
(popupClose)

Emits when the popup closes

Type
void
Default
—

ng-marker-tooltip

A tooltip shown while the marker is hovered or focused.

Prop
[offset]

Pixel offset from the marker

Type
Offset
Default
16
[anchor]

Side the tooltip anchors to (creation only)

Type
PositionAnchor
Default
auto
[className]

Extra classes for the tooltip pill

Type
string
Default
—

ng-map-popup

A standalone popup at a coordinate, with no marker.

Prop
[longitude]

Longitude

Type
number
Default
—
[latitude]

Latitude

Type
number
Default
—
[(open)]

Whether the popup is shown

Type
boolean
Default
true
[closeButton]

Shows a close button

Type
boolean
Default
false
[offset]

Pixel offset from the position

Type
Offset
Default
16
[maxWidth]

CSS max-width of the popup

Type
string
Default
'none'
[closeOnClick]

Closes on map click (creation only)

Type
boolean
Default
true
[labels]

Label overrides for the close button

Type
Partial<MapLabels>
Default
—
[className]

Extra classes for the popup card

Type
string
Default
—
Event
(popupClose)

Emits when the popup closes

Type
void
Default
—

ng-map-route

A line through a list of coordinates.

Prop
[coordinates]

Path as [longitude, latitude] pairs

Type
[number, number][]
Default
—
[id]

Id prefix of the source and layer. Changing it recreates them

Type
string
Default
auto
[color]

Line color

Type
string
Default
'#4285F4'
[width]

Line width in pixels

Type
number
Default
3
[opacity]

Line opacity

Type
number
Default
0.8
[dashArray]

Dash pattern as [dash, gap]

Type
[number, number]
Default
—
[interactive]

Responds to mouse events

Type
boolean
Default
true
[progress]

Covered fraction (0–1), drawn by ng-route-progress

Type
number
Default
—
[active]

Applies the active* paint

Type
boolean
Default
false
[activeColor]

Color while active

Type
string
Default
—
[activeWidth]

Width while active

Type
number
Default
—
[activeOpacity]

Opacity while active

Type
number
Default
—
[activeDashArray]

Dash pattern while active

Type
[number, number]
Default
—
[beforeId]

Layer id to insert the route before (creation only)

Type
string
Default
—
Event
(routeClick)

Emits when the route is clicked

Type
MapMouseEvent
Default
—
(routeMouseEnter)

Emits when the pointer enters

Type
MapMouseEvent
Default
—
(routeMouseLeave)

Emits when the pointer leaves

Type
MapMouseEvent
Default
—

ng-route-progress

The covered part of the parent route, up to its [progress].

Prop
[color]

Line color

Type
string
Default
'#22c55e'
[width]

Line width in pixels

Type
number
Default
width of the route
[opacity]

Line opacity

Type
number
Default
1
[dashArray]

Dash pattern as [dash, gap]

Type
[number, number]
Default
—

ng-route-marker

A marker placed along the parent ng-map-route.

Prop
[at]

Position along the route; a number is a fraction

Type
'start' | 'end' | 'progress' | number
Default
'start'

ng-map-arc

Curved arcs between origin and destination points, such as flight paths.

Prop
[data]

The arcs; extra keys reach events and paint

Type
MapArcDatum[]
Default
—
[id]

Id prefix of the source and layers. Changing it recreates them

Type
string
Default
auto
[curvature]

Bend relative to length; 0 is straight

Type
number
Default
0.2
[samples]

Segments per arc

Type
number
Default
64
[paint]

Line paint; read datum keys with ["get", key]

Type
MapLibre paint properties
Default
—
[layout]

Line layout

Type
MapLibre line layout
Default
—
[hoverPaint]

Paint of the hovered arc

Type
MapLibre paint properties
Default
—
[interactive]

Responds to mouse events

Type
boolean
Default
true
[beforeId]

Layer id to insert the arc layers before (creation only)

Type
string
Default
—
Event
(arcClick)

Emits the clicked arc

Type
MapArcEvent
Default
—
(arcHover)

Emits the hovered arc, or null on leave

Type
MapArcEvent | null
Default
—

ng-map-geojson

Renders GeoJSON as fill and outline layers.

Prop
[data]

GeoJSON object, or a URL to fetch

Type
MapGeoJSONData | string
Default
—
[id]

Id prefix of the source and layers. Changing it recreates them

Type
string
Default
auto
[promoteId]

Property used as feature id. Required for hover

Type
string
Default
—
[fillPaint]

Fill paint; false omits the layer

Type
MapLibre paint properties
Default
—
[linePaint]

Outline paint; false omits the layer

Type
MapLibre paint properties
Default
—
[fillHoverPaint]

Paint of the hovered feature. Requires promoteId

Type
MapLibre paint properties
Default
—
[interactive]

Responds to mouse events

Type
boolean
Default
false
[beforeId]

Layer id to insert the layers before (creation only)

Type
string
Default
—
Event
(featureClick)

Emits the clicked feature

Type
MapGeoJSONEvent
Default
—
(featureHover)

Emits the hovered feature, or null on leave

Type
MapGeoJSONEvent | null
Default
—

ng-map-cluster-layer

Clustered points with count labels and threshold-based colors and sizes.

Prop
[data]

Points, or a URL to fetch

Type
FeatureCollection<Point> | string
Default
—
[id]

Id prefix of the source and layers. Changing it recreates them

Type
string
Default
auto
[clusterMaxZoom]

Highest zoom that still clusters

Type
number
Default
14
[clusterRadius]

Cluster radius in pixels

Type
number
Default
50
[clusterColors]

Small, medium and large cluster colors

Type
[string, string, string]
Default
['#22c55e', '#eab308', '#ef4444']
[clusterThresholds]

Counts where medium and large start

Type
[number, number]
Default
[100, 750]
[pointColor]

Color of an unclustered point

Type
string
Default
'#3b82f6'
[zoomOnClusterClick]

Zooms into a clicked cluster

Type
boolean
Default
true
[beforeId]

Layer id to insert the cluster layers before (creation only)

Type
string
Default
—
Event
(pointClick)

Emits the clicked point

Type
ClusterPointClickEvent
Default
—
(clusterClick)

Emits the clicked cluster

Type
ClusterClickEvent
Default
—

ng-map-heatmap

A point-density heatmap with a theme-aware gradient.

Prop
[data]

Points, or a URL to fetch

Type
FeatureCollection<Point> | string
Default
—
[id]

Id prefix of the source and layer. Changing it recreates them

Type
string
Default
auto
[weightProperty]

Property used as point weight

Type
string
Default
—
[weightRange]

Weight [min, max], mapped to 0–1

Type
[number, number]
Default
[0, 1]
[radius]

Point radius in pixels

Type
number | Expression
Default
30
[intensity]

Global intensity multiplier

Type
number | Expression
Default
1
[opacity]

Layer opacity

Type
number | Expression
Default
1
[gradient]

Color ramp over the density

Type
HeatmapGradient
Default
—
[minZoom]

Lowest visible zoom

Type
number
Default
0
[maxZoom]

Zoom from which it is hidden

Type
number
Default
24
[paint]

Heatmap paint, merged last

Type
MapLibre paint properties
Default
—
[beforeId]

Layer id to insert the layer before (creation only)

Type
string
Default
—

ng-map-raster-source

A raster tile source (XYZ, TMS or WMS) and its layer.

Prop
[id]

Id prefix of the source and layer. Changing it recreates them

Type
string
Default
auto
[tiles]

Tile URL templates

Type
string[]
Default
—
[url]

TileJSON URL, instead of tiles

Type
string
Default
—
[tileSize]

Tile size in pixels (creation only)

Type
number
Default
256
[minzoom]

Lowest tile zoom (creation only)

Type
number
Default
0
[maxzoom]

Highest tile zoom (creation only)

Type
number
Default
22
[attribution]

Attribution text (creation only)

Type
string
Default
—
[bounds]

Tile bounds [west, south, east, north] (creation only)

Type
[number, number, number, number]
Default
—
[scheme]

Tile scheme (creation only)

Type
'xyz' | 'tms'
Default
'xyz'
[opacity]

Layer opacity

Type
number | Expression
Default
1
[visible]

Shows the layer; the source stays loaded

Type
boolean
Default
true
[paint]

Raster paint, merged last

Type
MapLibre paint properties
Default
—
[beforeId]

Layer id to insert the layer before (creation only)

Type
string
Default
—

ng-map-vector-source

A vector tile source (MVT, TileJSON or pmtiles://), drawn by a layer component.

Prop
[id]

Id prefix of the source. Changing it recreates them

Type
string
Default
auto
[tiles]

Tile URL templates

Type
string[]
Default
—
[url]

TileJSON or pmtiles:// URL. Overrides tiles

Type
string
Default
—
[minzoom]

Lowest tile zoom (creation only)

Type
number
Default
0
[maxzoom]

Highest tile zoom (creation only)

Type
number
Default
22
[attribution]

Attribution text (creation only)

Type
string
Default
—
[bounds]

Tile bounds [west, south, east, north] (creation only)

Type
[number, number, number, number]
Default
—
[scheme]

Tile scheme (creation only)

Type
'xyz' | 'tms'
Default
'xyz'
[promoteId]

Property used as feature id, for feature-state

Type
PromoteIdSpecification
Default
—

ng-map-image-overlay

An image pinned to four corner coordinates.

Prop
[id]

Id prefix of the source and layer. Changing it recreates them

Type
string
Default
auto
[url]

Image URL

Type
string
Default
—
[coordinates]

Four corners, clockwise from top left

Type
ImageOverlayCoordinates
Default
—
[opacity]

Layer opacity

Type
number | Expression
Default
1
[visible]

Shows the layer

Type
boolean
Default
true
[paint]

Raster paint, merged last

Type
MapLibre paint properties
Default
—
[beforeId]

Layer id to insert the layer before (creation only)

Type
string
Default
—

ng-map-fill-extrusion

3D extruded polygons, such as buildings; tilt the map to see them.

Prop
[id]

Id prefix of the layer. Changing it recreates them

Type
string
Default
auto
[source]

Id of the source to extrude

Type
string
Default
'carto'
[sourceLayer]

Layer of a vector source

Type
string
Default
'building' for carto
[heightProperty]

Property with the height in meters

Type
string
Default
'render_height'
[baseProperty]

Property with the base height in meters

Type
string
Default
'render_min_height'
[color]

Fill color

Type
string | Expression
Default
—
[opacity]

Layer opacity

Type
number
Default
0.8
[minZoom]

Lowest visible zoom

Type
number
Default
14
[maxZoom]

Zoom from which it is hidden

Type
number
Default
24
[filter]

MapLibre filter; null for none

Type
FilterSpecification | null
Default
—
[paint]

Fill-extrusion paint, merged last

Type
MapLibre paint properties
Default
—
[beforeId]

Layer id to insert the layer before (creation only)

Type
string
Default
—

ng-map-terrain

3D terrain from a raster-dem source, with an optional hillshade. One per map.

Prop
[id]

Id prefix of the sources and layers. Changing it recreates them

Type
string
Default
auto
[tiles]

Elevation tile URL templates

Type
string[]
Default
—
[url]

TileJSON URL, instead of tiles

Type
string
Default
—
[encoding]

Elevation tile encoding

Type
'terrarium' | 'mapbox'
Default
'terrarium'
[tileSize]

Tile size in pixels

Type
number
Default
256
[minzoom]

Lowest elevation tile zoom

Type
number
Default
0
[maxzoom]

Highest elevation tile zoom

Type
number
Default
15
[attribution]

Attribution text

Type
string
Default
—
[exaggeration]

Vertical scale; 1 is true scale

Type
number
Default
1
[hillshade]

Draws a hillshade layer

Type
boolean
Default
false
[hillshadeExaggeration]

Hillshade intensity (0–1)

Type
number
Default
0.5
[hillshadeShadowColor]

Color of shaded slopes

Type
string
Default
—
[hillshadeHighlightColor]

Color of lit slopes

Type
string
Default
—
[hillshadeAccentColor]

Accent color of steep slopes

Type
string
Default
—
[hillshadePaint]

Hillshade paint, merged last

Type
MapLibre paint properties
Default
—
[beforeId]

Layer id to insert the hillshade layer before (creation only)

Type
string
Default
—

provideZardMap(config?)

Registers the map defaults; provide it on the lazy map route, not in app.config.ts.

Prop
style

Id of the starting preset

Type
string
Default
'carto-positron'
keys

API keys of key-based providers

Type
Partial<Record<'maptiler' | 'stadia', string>>
Default
{}
persist

Saves the pick in localStorage; a string sets the key

Type
boolean | string
Default
false
presets

Presets the picker lists

Type
readonly ZardMapPreset[]
Default
ZARD_MAP_PRESETS
styles

Fixed basemap per theme. Overrides the service and picker

Type
MapStylesConfig | Signal<MapStylesConfig | undefined> | (() => Signal<MapStylesConfig | undefined>)
Default
—
labels

Translations for tooltips, loader and close button

Type
Partial<MapLabels> | Signal<Partial<MapLabels> | undefined> | (() => Signal<Partial<MapLabels> | undefined>)
Default
English

ZardMapStyleService

Root service holding the basemap preset of every map.

Prop
select(id, key?)

Switches every map to a preset

Type
ZardMapSelectResult
Default
—
setKey(provider, key)

Stores a provider API key; empty forgets it

Type
void
Default
—
reset()

Restores the default preset

Type
void
Default
—
presetId

Id of the preset in use

Type
Signal<string>
Default
'carto-positron'
preset

Preset in use

Type
Signal<ZardMapPreset>
Default
—
presets

Presets the picker lists

Type
Signal<readonly ZardMapPreset[]>
Default
ZARD_MAP_PRESETS
styles

Light and dark styles of the preset in use

Type
Signal<ZardMapStyles>
Default
—
keys

Known API keys, by provider

Type
Signal<ZardMapKeys>
Default
{}

z-map-style-select

Picks the basemap of every map on the page; maps with their own [styles] ignore it.

Prop
[class]

Custom CSS classes

Type
ClassValue
Default
''
[zAriaLabel]

Accessible name, caption and popover title

Type
string
Default
'Map style'
[zAlign]

Button edge the popover aligns to

Type
'start' | 'center' | 'end'
Default
'end'
(zStyleChange)

Emits the picked preset once applied

Type
ZardMapPreset
Default
—

ng-map-draw

Draws, selects and edits points, lines and polygons; install it with zard-cli add map-draw.

Prop
[modes]

Shapes the user can draw

Type
readonly ('point' | 'line' | 'polygon')[]
Default
['point', 'line', 'polygon']
[(mode)]

Active tool; static only displays features

Type
'point' | 'line' | 'polygon' | 'select' | 'static'
Default
'static'
[(features)]

Drawn features; the source of truth

Type
MapDrawFeatureCollection
Default
empty collection
[editable]

Enables select mode for editing features

Type
boolean
Default
false
[color]

Accent color as #rrggbb

Type
DrawHexColor
Default
—
Event
(drawEnd)

Emits when a shape is completed

Type
MapDrawEvent
Default
—
(featureEdit)

Emits when a feature is edited

Type
MapDrawEvent
Default
—
(featureSelect)

Emits the selected feature

Type
MapDrawFeature
Default
—
(featureDeselect)

Emits the deselected feature id

Type
string | number
Default
—
github iconwhatsapp icondiscord iconX icon

Made with ❤ in Brazil. Open source and available on GitHub .