Advanced Usage

Docs / Advanced

Reactivity

Everything inside <x-map> reacts to your Livewire state. The map element uses wire:ignore.self and only the canvas MapLibre draws into is wire:ignored. When your component re-renders, Livewire morphs the child markers, routes, arcs, GeoJSON and cluster layers in place. They are added, updated or removed on the live map, and the camera stays where the user left it.

Three rules keep updates smooth:

  • Don't wrap the map in wire:ignore. Older versions needed it, but now it stops children from updating.
  • Give the map a stable id. A user-supplied id is rendered on the element and Livewire keys the morph on it. It is also the mapId you use to target the map.
  • Use wire:key (and a stable id) on items rendered in @foreach loops so each item keeps its identity when the array changes.
php — Livewire Component
use Livewire\Component;

class StoreLocator extends Component
{
    public array $stores = [
        ['id' => 1, 'name' => 'Soho', 'lat' => 51.513, 'lng' => -0.136],
        ['id' => 2, 'name' => 'Shoreditch', 'lat' => 51.526, 'lng' => -0.078],
    ];

    public function addStore(float $lat, float $lng): void
    {
        $this->stores[] = [
            'id' => uniqid(),
            'name' => 'New store',
            'lat' => $lat,
            'lng' => $lng,
        ];
    }

    public function removeLast(): void
    {
        array_pop($this->stores);
    }
}
blade — store-locator.blade.php
<div>
    <button wire:click="removeLast">Remove last store</button>

    <!-- No wire:ignore: the markers below update in place -->
    <x-map id="stores-map" :center="[-0.1, 51.515]" :zoom="12" height="400px">
        @foreach ($stores as $store)
            <x-map-marker
                wire:key="store-{{ $store['id'] }}"
                :id="'store-'.$store['id']"
                :lat="$store['lat']"
                :lng="$store['lng']"
            >
                <x-marker-tooltip :text="$store['name']" />
            </x-map-marker>
        @endforeach
    </x-map>
</div>
Arcs, GeoJSON and cluster layers re-read their :data on every re-render, so updating the bound property is usually all you need. The map:update-*-{id} commands are still available when you want to push data without re-rendering.

Livewire Interactivity

The package dispatches events to Livewire and listens for specific commands, so you can drive the map from your PHP component.

Events Dispatched to Livewire

Each event is dispatched once as a bubbling DOM CustomEvent that Livewire also receives. That means Alpine (@map:click) and Livewire (#[On('map:click')]) listeners both get it exactly once. Every payload also includes mapId, the id of the map that fired it.

php — Livewire Component
use Livewire\Attributes\On;

#[On('map:marker-clicked')]
public function markerClicked(string $id, float $lat, float $lng, ?string $mapId = null): void
{
    $this->selectedStore = $id;
}

Map events

EventPayloadDescription
map:loadedcenter, zoomThe map is created and its first style has loaded
map:style-loadedstyle, darkA style finished loading (also after theme switches)
map:style-reloaded—The style was replaced via map:set-style
map:theme-changeddarkThe resolved light/dark theme changed (theme="auto")
map:clicklat, lng, lngLat, pointUser clicked on the map
map:double-clicklat, lngUser double-clicked on the map
map:right-clicklat, lngUser right-clicked (context menu) on the map
map:movelat, lngFired continuously while the map moves (throttled)
map:center-changedlat, lngFired once movement ends
map:bounds-changednorth, south, east, westVisible bounds after movement ends
map:viewport-changedcenter, zoom, bearing, pitchFull camera state after movement ends; center is [lng, lat]
map:zoomzoomFired continuously while zooming (throttled)
map:zoom-changedzoomFired once zooming ends
map:drag-endlat, lng, zoomUser finished dragging the map
map:bearing-changedbearingThe map was rotated (throttled)
map:pitch-changedpitchThe map was tilted (throttled)
map:3d-toggledenabledThe 3D button of <x-map-controls :three-d="true"> was toggled
map:locate-successlat, lng, accuracyThe locate control found the user
map:locate-errorcode, messageGeolocation failed or was denied
map:{event}MapLibre event dataExtra MapLibre events forwarded via custom_events / :events

Marker & popup events

EventPayloadDescription
map:marker-clickedid, lat, lngA marker was clicked
map:marker-mouseenterid, lat, lngPointer entered a marker
map:marker-mouseleaveidPointer left a marker
map:marker-drag-startid, lat, lngDragging a draggable marker started
map:marker-dragid, lat, lngFired while a marker is dragged (throttled)
map:marker-drag-endid, lat, lngA draggable marker was released
map:marker-popup-openidA marker popup opened
map:marker-popup-closeidA marker popup closed
map:popup-openidA standalone <x-map-popup> opened
map:popup-closeidA standalone popup closed

Route events

EventPayloadDescription
map:route-clickedid, lat, lng, coordinates, pointA route line was clicked
map:route-mouseenteridPointer entered a route
map:route-mouseleaveidPointer left a route
map:route-directions-readyid, geometry, distance, duration, alternativesDirections were fetched from OSRM
map:route-directions-errorid, messageFetching directions failed (falls back to straight lines)
map:route-alternative-selectedid, alternativeIndex, previousIndex, geometry, distance, durationAn alternative route became the primary route
map:route-updatedidA map:update-route-data-{id} command was applied
map:route-group-selection-changedgroupId, selectedRouteId, selectedIndex, previousRouteId, previousIndex, distance, durationThe selected route of a route group changed
map:route-group-directions-readygroupId, routesAll routes of a group were resolved
map:route-list-selectedrouteId, selectedIndex, previousIndex, distance, durationA route was picked in <x-map-route-list> (no mapId)

Arc, GeoJSON & cluster events

EventPayloadDescription
map:arc-clickedid, arc, lat, lngAn arc was clicked; arc is the original datum
map:arc-hoveredid, arc, lat, lngPointer moved onto an arc; on leave arc is null
map:geojson-clickedid, feature, lat, lngA feature was clicked; feature is {id, properties, geometry}
map:geojson-hoveredid, feature, lat, lngPointer moved onto a feature; on leave feature is null
map:cluster-clickedcluster_id, lat, lng, countA cluster bubble was clicked
map:cluster-expandedcluster_id, zoomThe map zoomed in to expand a cluster
map:cluster-point-clickedfeature_id, lat, lng, propertiesAn unclustered point was clicked

Commands from Livewire

Trigger these from your Livewire component using $this->dispatch(). You can pass the payload as named arguments or as a single array.

Map commands

Each accepts an optional mapId. Without it the command applies to every map on the page.

CommandPayloadDescription
map:fly-tocenter, zoom, bearing, pitch, …Animate the camera along a flight path (MapLibre flyTo options)
map:ease-tocenter, zoom, bearing, pitch, …Animate the camera smoothly (MapLibre easeTo options)
map:jump-tocenter, zoom, bearing, pitch, …Move the camera instantly (MapLibre jumpTo options)
map:fit-boundsbounds, padding, …Fit [[west, south], [east, north]] in view; padding defaults to 40
map:set-zoomzoomSet the zoom level
map:set-centercenterSet the center ([lng, lat])
map:set-bearingbearingSet the rotation in degrees
map:set-pitchpitchSet the tilt in degrees
map:set-stylestyleReplace the style (URL or style JSON); fires map:style-reloaded
map:set-themethemeSwitch to light, dark or auto
map:resize—Recalculate the map size (after showing a hidden container)
map:callmethod, argsCall any public MapLibre Map method, e.g. method: 'setMaxZoom', args: [12]

Component commands

These target a component by the id you gave it, so give dynamic routes, arcs, GeoJSON and cluster layers an explicit id.

CommandPayloadDescription
map:update-route-data-{id}coordinates, color, width, progress, progressDuration, …Update a route's coordinates and/or style
map:route-progress-{id}progress, durationAnimate the progress overlay to progress (0–1) over duration ms
map:select-route-{id}indexMake an alternative route the primary one
map:force-animateid?, duration?Replay the draw animation of one route/group, or of all animated ones
map:update-route-group-{id}selectedRoute, routesChange the selected route or patch route styles in a group
map:update-arc-data-{id}data (or a plain array)Replace the arcs of an <x-map-arc>
map:update-geojson-{id}data (or a GeoJSON object)Replace the data of an <x-map-geojson>
map:update-cluster-data-{id}data (or a FeatureCollection)Replace the points of a cluster layer (send GeoJSON, e.g. GeoJSON::fromArray($points))

Targeting a Map

When a page has more than one map, pass mapId with a command to control only that map. Map commands without mapId apply to every map on the page. Outbound events carry the same mapId, so a listener can tell which map fired them.

php — Livewire Component
use Livewire\Attributes\On;

public function showLondon(): void
{
    // Only the map with id="main-map" moves
    $this->dispatch('map:fly-to', mapId: 'main-map', center: [-0.09, 51.5], zoom: 12);
}

#[On('map:click')]
public function mapClicked(float $lat, float $lng, ?string $mapId = null): void
{
    if ($mapId !== 'main-map') {
        return; // ignore clicks on the overview map
    }

    $this->addStore($lat, $lng);
}
blade
<x-map id="main-map" height="400px" />
<x-map id="overview-map" height="200px" :interactive="false" />
Component commands like map:update-route-data-{id} are already scoped by the component id in the event name and don't need mapId.

Example: Fly To Location

Smoothly animate the map to a new location from your Livewire component:

php — Livewire Component
use Livewire\Component;

class FlightTracker extends Component
{
    public function centerOnPilot()
    {
        $this->dispatch('map:fly-to',
            mapId: 'flight-map',
            center: [-0.09, 51.5],
            zoom: 12,
            essential: true,
        );
    }

    public function render()
    {
        return view('livewire.flight-tracker');
    }
}
blade — flight-tracker.blade.php
<div>
    <button wire:click="centerOnPilot">
        Center on Pilot
    </button>

    <x-map
        id="flight-map"
        :center="[-0.09, 51.5]"
        :zoom="8"
        height="400px"
    >
        <x-map-controls />
    </x-map>
</div>

Theme Awareness

With theme="auto" (the default) the map follows a dark/light class or data-theme attribute on your <html> element, and fires map:theme-changed when it switches. You can also force a specific theme, or change it at runtime with the map:set-theme command:

theme="light"

theme="dark"

Controlled Viewport

Track and control the map viewport (center, zoom, bearing, pitch) by listening to events and using Alpine.js for real-time reactivity. To sync the camera to Livewire, listen to map:viewport-changed. It fires once per movement with the full camera state, so it doesn't flood the server.

lng: lat: zoom: bearing: ° pitch: °
php — sync the camera to Livewire
#[On('map:viewport-changed')]
public function viewportChanged(array $center, float $zoom, float $bearing, float $pitch): void
{
    [$this->lng, $this->lat] = $center;
    $this->zoom = $zoom;
}

Viewport Event Reference

EventDetailDescription
map:movelat, lngFired continuously as the map moves
map:center-changedlat, lngFired once movement ends
map:bounds-changednorth, south, east, westFired once movement ends
map:viewport-changedcenter, zoom, bearing, pitchFull camera state, fired once movement ends
map:zoomzoomFired continuously during zoom
map:zoom-changedzoomFired once zoom ends
map:bearing-changedbearingFired when the map is rotated
map:pitch-changedpitchFired when the map is tilted

Example: Fit Bounds

Fit the map to show all your markers. padding defaults to 40, and any other MapLibre fitBounds option (e.g. maxZoom, duration) is passed through:

php — Livewire Component
public function showAllLocations()
{
    $this->dispatch('map:fit-bounds', [
        'bounds' => [
            [-0.15, 51.49],  // Southwest corner [lng, lat]
            [-0.01, 51.54],  // Northeast corner [lng, lat]
        ],
        'padding' => 50,
    ]);
}

Example: Set Zoom

Programmatically change the zoom level:

php — Livewire Component
public function zoomIn()
{
    $this->dispatch('map:set-zoom', [
        'zoom' => 16,
    ]);
}

Example: Resize

The map resizes itself when its own element changes size. If its container was hidden when the page loaded (tabs, accordions, modals), force a recalculation once it's visible:

php — Livewire Component
public function onTabChanged()
{
    $this->dispatch('map:resize');
}
The map:resize dispatch is essential when the map is inside a container that was hidden on page load (e.g., an inactive tab or collapsed accordion). MapLibre needs a resize event to calculate tile positions correctly. For client-side toggles, use the x-map-resize directive below instead.

The $map Magic

Inside any markup in an <x-map>, the Alpine magic $map returns the MapLibre Map instance of the nearest enclosing map. That gives you the full MapLibre API from a button, with no JavaScript file and no server round-trip. It pairs well with <x-map-control> for custom buttons. $map is null until the map has been created.

JavaScript API

For code outside the map markup (your own scripts, other Alpine components), the package exposes window.LivewireMap:

MemberReturnsDescription
getMap(id)Map | nullThe MapLibre instance of the map with that id, or null if it doesn't exist (yet)
getRoute(id)controller | nullThe controller of the <x-map-route> with that id
waitForMap(id, callback)—Calls callback(map) once the map exists and its style has loaded. Works across wire:navigate. Pass null as the id for the first map on the page
mapsobjectRegistry of all maps, keyed by id
versionstringThe package's JavaScript version

Route controller

MethodReturnsDescription
setProgress(value, durationMs = 0)—Set the progress overlay (0–1, or null to clear), animated over durationMs
selectRoute(index)—Make an alternative route the primary one
animate(duration?)—Replay the draw animation
pointAt(at)[lng, lat] | nullPoint at 'start', 'end', 'progress' or a 0–1 fraction
fitBounds(options?)—Fit the map to the route
js
// Run code once the map is ready (also after wire:navigate)
LivewireMap.waitForMap('main-map', (map) => {
    map.addControl(new maplibregl.AttributionControl({ compact: true }));
});

// Move a delivery along its route
const route = LivewireMap.getRoute('delivery');
route?.setProgress(0.75, 1500);
const [lng, lat] = route?.pointAt('progress') ?? [];

Alpine.js Directives

x-map-resize resizes the enclosing map whenever its expression becomes truthy. It's useful for maps in tabs, modals and accordions that toggle on the client. For other camera actions use $map (see above).

DirectiveValueDescription
x-map-resizeexpressionResize the enclosing map each time the expression becomes truthy
blade
<div x-data="{ open: false }">
    <button x-on:click="open = !open">Toggle map</button>

    <div x-show="open">
        <x-map height="300px">
            <div x-map-resize="open"></div>
        </x-map>
    </div>
</div>

Custom MapLibre Events

Forward any other MapLibre event as map:{event}. Set it for all maps with the custom_events config key, or per map with the :events prop (the two are merged):

php — config/livewire-mapcn.php
'custom_events' => ['idle', 'sourcedata'],
blade
<x-map :events="['idle', 'rotatestart']" @map:idle="console.log('map is idle')" />

GeoJSON Helper

The package includes a support class that converts PHP arrays (items with lat/lng keys, or GeoJSON Features) into a GeoJSON FeatureCollection:

php
use Kwasii\LivewireMapcn\Support\GeoJSON;

$data = [
    ['name' => 'London', 'lat' => 51.5, 'lng' => -0.1],
    ['name' => 'Paris', 'lat' => 48.8, 'lng' => 2.3],
];

$geojson = GeoJSON::fromArray($data);
// Returns a valid GeoJSON array structure

Performance Tips

Clustering Precision

Tune :cluster-radius (higher is faster) and :cluster-max-zoom to balance performance and detail.

Simplify Geometry

Increase :tolerance (e.g., 1.0) to simplify GeoJSON geometry or :buffer to reduce off-screen flashing.

Throttled Events

Prefer end-of-gesture events (map:viewport-changed, map:zoom-changed) for Livewire listeners, and use Alpine's .throttle or .debounce for continuous events like @map:move.

Dynamic Data

For large, frequently changing datasets, push data with map:update-cluster-data-{id}, map:update-geojson-{id} or map:update-arc-data-{id} from a #[Renderless] action instead of re-rendering the component.