larswiegers/laravel-maps
Laravel Maps lets you add Leaflet.js or Google Maps to any Laravel app with minimal setup. Supports centerpoints, markers, bounds, zoom levels, and custom tiles like OpenStreetMap and Mapbox.
Install and see it render in ~5 minutes.
composer require larswiegers/laravel-maps
php artisan vendor:publish --provider="Larswiegers\LaravelMaps\LaravelMapsServiceProvider"
Publishing is optional — it only exposes the package's Blade views so you can override markup, script tags, and container classes. Do it early anyway; the views are the real documentation for what attributes are supported and how the JS is wired.
Credentials go in .env, not in a config file:
MAPS_GOOGLE_MAPS_ACCESS_TOKEN=your-google-key # enable "Maps JavaScript API" in Google Cloud + billing
MAPS_MAPBOX_ACCESS_TOKEN=your-mapbox-key # only if you swap Leaflet's tile host to Mapbox
Leaflet + OpenStreetMap needs no key at all, which makes it the right default for local development and for the first thing you ship.
First use case — drop a single marker on a page:
<x-maps-leaflet
:centerPoint="['lat' => 52.16, 'long' => 5]"
:zoomLevel="13"
:markers="[['lat' => 52.1644, 'long' => 5.9856]]"
/>
Where to look first, in order:
resources/views/vendor/laravel-maps/...) — every attribute the component accepts is visible there.CHANGELOG.md / releases — this package has shipped breaking-ish changes (unpinned Leaflet version in v0.15, deprecated google.maps.Marker → Advanced Markers in v0.20). Skim before upgrading.The realistic day-one use case is a store/location locator: a controller hands the view a collection of addresses, the view maps them to ['lat' => ..., 'long' => ...] arrays, one component call renders the map.
// Controller
public function show(Store $store)
{
return view('stores.show', [
'markers' => $store->locations->map(fn ($l) => [
'lat' => (float) $l->latitude,
'long' => (float) $l->longitude,
'title' => $l->name,
])->all(),
]);
}
<x-maps-google :markers="$markers" :fitToBounds="true" :zoomLevel="6" />
Cast to float. Decimal columns come back as strings from the DB, and you do not want to discover that in a JS console.
:fitToBounds="true" — zoom/pan so all markers are visible. Right choice when the marker count is unknown and you want a guaranteed full view.:centerToBoundsCenter="true" — centers on the geometric midpoint but keeps your zoomLevel. Right choice when you want a consistent zoom (e.g. always a regional overview) regardless of marker spread.centerPoint. Use this for a single-location page where you know exactly where the pin should sit.Do not stack fitToBounds and centerToBoundsCenter expecting a hybrid; pick one.
<x-maps-google :markers="[[
'lat' => 52.1644,
'long' => 5.9856,
'title' => 'HQ',
'icon' => 'images/pin-hq.png',
'label' => 'Main Store',
'info' => 'Open 9–5, Mon–Fri',
]]" />
You can combine icon + label + info on the same marker. info gives you a click-triggered info window; label gives you always-visible text via Advanced Markers. For Leaflet, popups are the equivalent (bound to marker instances since v0.10) — but check the published view, since the attribute names differ between the two component families.
Leaflet only:
<x-maps-leaflet id="map-offices" :markers="$offices" />
<x-maps-leaflet id="map-warehouse" :markers="$warehouse" />
The id is required for the second and subsequent maps. Without it, both components emit the same default container/element id and the second script instance binds to the first div. Google Maps explicitly does not support multiple maps per page — if the design needs two maps, use Leaflet for both or render one Google map and a static image for the second.
Since v0.21 the Leaflet instance is exposed via a custom event, so you can attach your own listeners without forking the view:
document.addEventListener('leaflet:loaded', (e) => {
// e.detail (or your published view's payload) → map.addLayer(...) / map.on('moveend', ...)
});
Check the published view for the exact event name/payload in your installed version rather than trusting the snippet — this is the sanctioned extension point and it's the one place where reading the view beats reading the README.
<x-maps-leaflet
:attribution="'Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Imagery © Mapbox'"
/>
With MAPS_MAPBOX_ACCESS_TOKEN present, Mapbox becomes the tile host. Mapbox's terms require visible attribution; the package provides a default, so only pass :attribution when you're overriding it — and if you override, keep both OSM and Mapbox credits.
<x-maps-leaflet leafletVersion="1.9.4" />
Default is "latest." The v0.15 release moved from a pinned 1.7.1 to latest and broke apps. In anything with a maintenance horizon, pin explicitly in a Blade partial/component wrapper of your own so there's one place to bump it.
Create resources/views/components/app-map.blade.php that hardcodes the version pin, the default zoom, your CSS classes, and any attribution, then delegates. Every feature page then uses <x-app-map> and a Leaflet/Google swap becomes a one-file change.
long, not lng. The marker key is long. Google's own docs use lng, so copy-pasted LatLng objects from elsewhere will silently render no markers — the JS reads undefined and skips the pin.auto, the map renders as a 0px-tall invisible div. The classic symptom is "the map works on one page but not another" — it's CSS, not the package. Set a height on the component's class/style (pass-through has been supported since v0.3).:attribution example is malformed HTML. Prefer single-quoted PHP strings with "/© entities, or build the string in a variable and pass :attribution="$attribution". Never interpolate user input into these attributes — the values land inside a <script> block, so treat every string (titles, labels, info text) as an injection vector and escape it.env() dies under config:cache. If you publish the views and add your own env('MAPS_...') calls, they return null in production. Read the token via a config entry (add your own config/maps.php mapping env(...)) instead.wire:ignore around the component plus a dispatch-driven re-init, or extract the map into a plain Blade partial rendered outside the Livewire tree.maps.googleapis.com). A strict CSP, an air-gapped deployment, or a European privacy review will block this. The mitigation is publishing the views and pointing the <script>/<link> tags at self-hosted copies — another reason to publish early even if you don't override anything yet.vendor/. Vendor edits vanish on composer update and are invisible in code review.composer.json, read the release notes before bumping, and keep your <x-app-map> wrapper as the single seam that absorbs a Leaflet↔Google swap or a major-version jump.How can I help you explore Laravel packages today?