MapLibre GL
MapLibre GL JS is the default
engine behind <x-wirekit::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.
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.
Install
# 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. Serve it from your own build, which is what step 1 put in
node_modules:
<!-- 2. Load MapLibre's JS + CSS BEFORE @wirekitScripts so window.maplibregl
exists when WireKit's map glue initializes. Vite emits both from the
`import 'maplibre-gl'` in your route entry. -->
@vite(['resources/js/maps.js'])
@wirekitScripts
If you load it from a CDN instead
A CDN is fine for a prototype, with one thing to get right: pin the version.
<!-- Replace X.Y.Z with the version in YOUR package.json — the one step 1
installed. An unpinned https://unpkg.com/maplibre-gl/… URL is not a
shortcut, it is a subscription: it serves whatever is newest at the moment
of the request, so the next major release reaches your users without
anyone deciding that it should, and it reaches them on a page you have
not tested it against. -->
<!-- 3. And an `integrity` hash beside the pin. A pinned URL fixes WHICH version the
CDN serves; it does not make the CDN trustworthy, and a script tag without SRI
executes whatever comes back. unpkg publishes the hash for any file — append
`?meta` to the URL — and the browser refuses the response when it does not
match. `crossorigin="anonymous"` is required for SRI to be checked at all. -->
<link href="https://unpkg.com/[email protected]/dist/maplibre-gl.css" rel="stylesheet"
integrity="sha384-REPLACE-WITH-THE-HASH-FOR-YOUR-VERSION" crossorigin="anonymous" />
<script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js" defer
integrity="sha384-REPLACE-WITH-THE-HASH-FOR-YOUR-VERSION" crossorigin="anonymous"></script>
@wirekitScripts
A pin fixes what is served; it does not verify it. Add an
integrity
attribute alongside it if a third party executing script in your origin is part
of your threat model — and this engine is by far the largest single piece of it.
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 — the full engine is several times the size of a raster alternative like Leaflet, and its own documentation carries the current figure. 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:
{{-- 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
{{-- A page that renders a map --}}
{{-- 2. Push the engine assets only from this page — they reach the layout's
head stack, so the engine ships on map routes only rather than on every page. --}}
@push('head')
<link href="https://unpkg.com/[email protected]/dist/maplibre-gl.css" rel="stylesheet"
integrity="sha384-REPLACE-WITH-THE-HASH-FOR-YOUR-VERSION" crossorigin="anonymous" />
<script src="https://unpkg.com/[email protected]/dist/maplibre-gl.js" defer
integrity="sha384-REPLACE-WITH-THE-HASH-FOR-YOUR-VERSION" crossorigin="anonymous"></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:
{{-- 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. Pick the style on the server, from
whatever your application already knows about the reader's theme, and pass it as
the styleUrl prop:
{{-- $dark is your application's own theme flag — a session value, a user
preference column, whatever decides the `.dark` class on <html>. --}}
<x-wirekit::map
:center="[52.5200, 13.4050]"
:zoom="11"
:style-url="$dark
? 'https://tiles.example.com/dark/style.json?key=YOUR_KEY'
: 'https://tiles.example.com/light/style.json?key=YOUR_KEY'"
/>
styleUrl is read once, when the map boots, and there is no client-side
attribute behind it: the value travels inside the component's Alpine payload,
which the server writes at render time. So x-bind:style-url does not switch the
style — it sets an HTML attribute nothing reads, styleUrl never reaches the
payload at all, and the map quietly falls back to the keyless demo tiles above.
A live theme toggle therefore has to re-create the map rather than re-style
it. Under Livewire, give the tag a wire:key that carries the theme, so a change
replaces the element instead of patching it and the map boots against the new
style:
<x-wirekit::map
wire:key="store-map-{{ $dark ? 'dark' : 'light' }}"
:center="[52.5200, 13.4050]"
:zoom="11"
:style-url="$dark ? $darkStyleUrl : $lightStyleUrl"
/>
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.
Interactive map needs a map library — the locations are listed alongside.
Map tiles are not loaded because your browser asked for reduced data. The locations are listed alongside.
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.
Interactive map needs a map library — the locations are listed alongside.
Map tiles are not loaded because your browser asked for reduced data. The locations are listed alongside.
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.)
Interactive map needs a map library — the locations are listed alongside.
Map tiles are not loaded because your browser asked for reduced data. The locations are listed alongside.
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).
Interactive map needs a map library — the locations are listed alongside.
Map tiles are not loaded because your browser asked for reduced data. The locations are listed alongside.
Locations ()
- No locations
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.
Interactive map needs a map library — the locations are listed alongside.
Map tiles are not loaded because your browser asked for reduced data. The locations are listed alongside.
Interactive map needs a map library — the locations are listed alongside.
Map tiles are not loaded because your browser asked for reduced data. The locations are listed alongside.
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
imageURL: 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.
Interactive map needs a map library — the locations are listed alongside.
Map tiles are not loaded because your browser asked for reduced data. The locations are listed alongside.
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 — the engine-independent API, markers, list, and Livewire wiring
- MapLibre GL JS documentation
- MapLibre styles & sources spec