---
title: MapLibre GL
description: Set up MapLibre GL as the map engine — install, map styles, dark mode, and real-world marker patterns
visibility: guest
draft: false
---

# MapLibre GL

[MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/) is the **default**
engine behind [`<x-wirekit::map>`](/components/map). It renders **vector** tiles,
so the map is fully styleable — you control the colors, fonts, and layers through
a style document rather than baked-in raster images. The engine is open source
under the **BSD-3-Clause** license and never enters WireKit's bundle; you load it
as a peer dependency.

::: info
This guide covers the MapLibre engine specifically. For the marker model, the
accessible list, the props, and Livewire wiring — all of which are
engine-independent — see the [Map overview](/components/map).
:::

## Install

```bash
# 1. Install the engine (a peer dependency — not bundled by WireKit).
npm install maplibre-gl
```

Then load the engine in your layout — order matters, WireKit's glue looks for
it at Alpine init:

```blade
<!-- 2. Load MapLibre's JS + CSS BEFORE @wirekitScripts so window.maplibregl
     exists when WireKit's map glue initializes. -->
<link href="https://unpkg.com/maplibre-gl/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl/dist/maplibre-gl.js"></script>
@wirekitScripts
```

`provider="maplibre"` is the default, so once `window.maplibregl` is present the
map upgrades from the marker-list fallback to a live MapLibre canvas
automatically — no prop changes needed.

## Bundle size

MapLibre GL is a large library (roughly 750 KB gzipped for the full engine). It
is **your app's** dependency, not WireKit's — WireKit never bundles it — so where
you load it is your call. Loading it globally (a `<script>` in every layout, or
an `import 'maplibre-gl'` in your shared `app.js`) puts that weight on every
page, even pages with no map. Load it only on the routes that actually render a
map instead.

The simplest approach is a per-page asset stack — the layout exposes a stack,
and only your map pages push the engine into it:

```blade
{{-- Layout (resources/views/layouts/app.blade.php) --}}

{{-- 1. Expose a head stack BEFORE @wirekitScripts so a page can inject the
        engine ahead of WireKit's map glue (which reads window.maplibregl at
        Alpine init). Non-map pages push nothing, so they never load it. --}}
@stack('head')
@wirekitScripts
```

```blade
{{-- A page that renders a map --}}

{{-- 2. Push the engine assets only from this page — they reach the layout's
        head stack, so the ~750 KB engine ships on map routes only. --}}
@push('head')
    <link href="https://unpkg.com/maplibre-gl/dist/maplibre-gl.css" rel="stylesheet" />
    <script src="https://unpkg.com/maplibre-gl/dist/maplibre-gl.js"></script>
@endpush

<x-wirekit::map :center="[51.5, -0.12]" :zoom="11" :markers="$stops" />
```

If you compile the engine through a bundler instead of a CDN, the same principle
applies: import `maplibre-gl` in a route-specific entry (or a dynamic
`import('maplibre-gl')` that resolves before the map mounts and assigns
`window.maplibregl`), never in the global app entry. Until the engine is present
the component shows its accessible marker list, so the page is never broken while
the engine loads.

## Map Styles

MapLibre reads a **style document** (a JSON describing sources + layers). Point
`styleUrl` at one:

```blade
{{-- A vector style from any provider. Many require an API key in the URL. --}}
<x-wirekit::map
    :center="[52.5200, 13.4050]"
    :zoom="12"
    style-url="https://tiles.example.com/streets/style.json?key=YOUR_KEY"
    :markers="$stores"
/>
```

With no `styleUrl`, the map falls back to MapLibre's **keyless demo tiles**
(`demotiles.maplibre.org`). Those are built from Natural Earth data for
demonstration only — great for a country-level overview, but they carry no
street-level detail. For real street maps, supply a `styleUrl` from a tile
provider (keyless options exist; most commercial providers issue an API key).

## Dark Mode

Dark mode is a different **tile style**, not a CSS filter over the canvas — a
filter would wash out labels and markers. Supply a dark `styleUrl` and switch it
when the `.dark` class is present:

```blade
{{-- Bind styleUrl to the active theme so the tiles match light/dark. --}}
<x-wirekit::map
    :center="[52.5200, 13.4050]"
    :zoom="11"
    x-bind:style-url="document.documentElement.classList.contains('dark')
        ? 'https://tiles.example.com/dark/style.json?key=YOUR_KEY'
        : 'https://tiles.example.com/light/style.json?key=YOUR_KEY'"
/>
```

## Store Locator

A multi-marker map is the classic store-locator surface: every location is a
themed marker on the canvas **and** a focusable row in the list. The list is what
a screen-reader user (or anyone) navigates; the map is the visual layer on top.

:::preview{title="Store locator with regional markers"}
<x-wirekit::map
    :center="[51.1657, 10.4515]"
    :zoom="5"
    aria-label="Store locations across Germany"
    list-label="Stores"
    :markers="[
        ['id' => 'b', 'lat' => 52.5200, 'lng' => 13.4050, 'label' => 'Kaufhaus Brandt — Berlin Mitte · Open', 'body' => 'Mo–Sa 9:00–20:00', 'intent' => 'success'],
        ['id' => 'm', 'lat' => 48.1351, 'lng' => 11.5820, 'label' => 'Brandt Stachus — Munich · Open', 'body' => 'Mo–Sa 9:30–20:00', 'intent' => 'success'],
        ['id' => 'h', 'lat' => 53.5511, 'lng' => 9.9937, 'label' => 'Brandt Hafencity — Hamburg · Busy', 'body' => 'Mo–Sa 10:00–19:00', 'intent' => 'warning'],
        ['id' => 'c', 'lat' => 50.9375, 'lng' => 6.9603, 'label' => 'Brandt Passage — Cologne · Closed today', 'body' => 'Tu–Sa 10:00–18:00', 'intent' => 'danger'],
        ['id' => 'f', 'lat' => 50.1109, 'lng' => 8.6821, 'label' => 'Brandt Zeil — Frankfurt · Open', 'body' => 'Mo–Sa 9:00–20:00', 'intent' => 'success'],
    ]"
/>
:::

## Color-Coded Markers

A marker's `intent` colors its list dot — `success` / `warning` / `danger` /
`accent` — so a status map (fleet tracking, service coverage, an incident board)
reads at a glance. The label always carries the status **in words** too, so the
meaning never rides on the dot color alone.

:::preview{title="Fleet status by color"}
<x-wirekit::map
    :center="[51.1657, 10.4515]"
    :zoom="5"
    aria-label="Fleet status"
    list-label="Vehicles"
    :markers="[
        ['id' => 't1', 'lat' => 52.5200, 'lng' => 13.4050, 'label' => 'Truck 1 — on route', 'intent' => 'success'],
        ['id' => 't2', 'lat' => 48.1351, 'lng' => 11.5820, 'label' => 'Truck 2 — delayed', 'intent' => 'warning'],
        ['id' => 't3', 'lat' => 50.1109, 'lng' => 8.6821, 'label' => 'Truck 3 — stopped', 'intent' => 'danger'],
    ]"
/>
:::

## Single Location

For a "where to find us" map, a single marker plus a tight `center`/`zoom` does
the job. The label carries the full address so the location is reachable from the
list without reading the map. (Supply a `styleUrl` for street-level tiles — the
keyless demo tiles shown here stop at a regional overview.)

:::preview{title="A single point of interest"}
<x-wirekit::map
    :center="[52.5200, 13.4050]"
    :zoom="6"
    aria-label="Where to find us"
    list-label="Visit us"
    :markers="[
        ['id' => 'hq', 'lat' => 52.5200, 'lng' => 13.4050, 'label' => 'Head office — Unter den Linden 1, 10117 Berlin', 'intent' => 'accent'],
    ]"
/>
:::

## Map Only

Need just the map? Pass `list="false"` to drop the sidebar — the canvas fills the
width and the marker list stays `sr-only` for assistive tech (the prop is on the
[Map overview](/components/map)).

:::preview{title="Map only — no sidebar"}
<x-wirekit::map
    list="false"
    :center="[51.1657, 10.4515]"
    :zoom="5"
    aria-label="Coverage area"
    :markers="[
        ['id' => 'b', 'lat' => 52.5200, 'lng' => 13.4050, 'label' => 'Berlin', 'intent' => 'accent'],
        ['id' => 'm', 'lat' => 48.1351, 'lng' => 11.5820, 'label' => 'Munich', 'intent' => 'accent'],
        ['id' => 'h', 'lat' => 53.5511, 'lng' => 9.9937, 'label' => 'Hamburg', 'intent' => 'accent'],
    ]"
/>
:::

## Selection Highlight

Selecting a store — by clicking its list row **or** its map pin — highlights the
matching row. Two styles via `highlight`: **`ring`** (the default — an inset
frame) or **`fill`** (a soft tinted-tile background). Either way `highlight-color`
(`accent` / `success` / `warning` / `danger` / `neutral`) sets the color so the
selection reads in your intent color rather than the near-black stock accent.
Click a row in each map below to compare.

:::preview{title="Ring selection (default)"}
<x-wirekit::map
    :center="[51.1657, 10.4515]"
    :zoom="5"
    aria-label="Service coverage with a ring highlight"
    list-label="Regions"
    highlight="ring"
    highlight-color="accent"
    :markers="[
        ['id' => 'n', 'lat' => 53.5511, 'lng' => 9.9937, 'label' => 'North hub — Hamburg'],
        ['id' => 'w', 'lat' => 50.9375, 'lng' => 6.9603, 'label' => 'West hub — Cologne'],
        ['id' => 's', 'lat' => 48.1351, 'lng' => 11.5820, 'label' => 'South hub — Munich'],
    ]"
/>
:::

:::preview{title="Fill selection in a chosen color"}
<x-wirekit::map
    :center="[51.1657, 10.4515]"
    :zoom="5"
    aria-label="Service coverage with a fill highlight"
    list-label="Regions"
    highlight="fill"
    highlight-color="success"
    :markers="[
        ['id' => 'n', 'lat' => 53.5511, 'lng' => 9.9937, 'label' => 'North hub — Hamburg'],
        ['id' => 'w', 'lat' => 50.9375, 'lng' => 6.9603, 'label' => 'West hub — Cologne'],
        ['id' => 's', 'lat' => 48.1351, 'lng' => 11.5820, 'label' => 'South hub — Munich'],
    ]"
/>
:::

## Pin Tooltips

Hovering (or tapping) a pin opens a bubble whose content follows the marker's
data shape — four variants, no extra configuration:

- **Text only** — just a `label`: a single bold line.
- **Styled text** — `label` + `body`: a small card with the name on top and a
  muted detail line (address, opening hours) below.
- **Text with photo** — add an `image` URL: the photo renders as a card above
  the text, like showing the storefront at its location.
- **Photo only** — `image` + `tooltip: 'image'`: the bubble is just the photo;
  the label still names the pin for screen readers and stays in the marker list.

Sanitize image URLs you don't control — the adapter escapes the `src`.

:::preview{title="One map, four tooltip variants"}
<x-wirekit::map
    :center="[52.5200, 13.4050]"
    :zoom="11"
    aria-label="Tooltip variants across Berlin"
    list-label="Stores"
    :markers="[
        ['id' => 'text', 'lat' => 52.5200, 'lng' => 13.4050, 'label' => 'Brandt Mitte — text only', 'intent' => 'accent'],
        ['id' => 'styled', 'lat' => 52.4990, 'lng' => 13.4030, 'label' => 'Brandt Kreuzberg — styled', 'body' => 'Mo–Sa 10:00–19:00 · Oranienstr. 12', 'intent' => 'success'],
        ['id' => 'photo', 'lat' => 52.5400, 'lng' => 13.4240, 'label' => 'Brandt Prenzlauer Berg — with photo', 'body' => 'Tu–Sa 11:00–19:00', 'image' => '/placeholder/320x180?bg=4f46e5&fg=ffffff&label=P-Berg&gradient=none', 'intent' => 'warning'],
        ['id' => 'imgonly', 'lat' => 52.5100, 'lng' => 13.3770, 'label' => 'Brandt Tiergarten — photo only', 'image' => '/placeholder/320x180?bg=0f766e&fg=ffffff&label=Tiergarten&gradient=none', 'tooltip' => 'image', 'intent' => 'neutral'],
    ]"
/>
:::

## Tiles, Attribution & Cost

The MapLibre **engine** is free (BSD-3-Clause), but the **tiles** it renders are a
separate concern with their own terms:

- **Demo tiles** (`demotiles.maplibre.org`) — keyless, Natural Earth data,
  intended for demos and testing only. Not for production.
- **Commercial providers** — issue an API key and bill by map load or tile
  request. They handle attribution requirements in their style documents.
- **Self-hosted tiles** — generate your own vector tiles (e.g. from OpenStreetMap
  data) and serve them yourself; you own the cost and the attribution.

Whatever the source, display its **required attribution** visibly on the map, and
read its usage policy before shipping at scale.

## License

MapLibre GL JS is distributed under the **BSD-3-Clause** license (a permissive
open-source license). WireKit treats it as an optional peer dependency — you
install and load it; WireKit never bundles or relicenses it.

## See Also

- [Map overview](/components/map) — the engine-independent API, markers, list, and Livewire wiring
- [MapLibre GL JS documentation](https://maplibre.org/maplibre-gl-js/docs/)
- [MapLibre styles & sources spec](https://maplibre.org/maplibre-style-spec/)
