diff --git a/.changeset/maps-maplibre-styles.md b/.changeset/maps-maplibre-styles.md
new file mode 100644
index 000000000..90d182bb6
--- /dev/null
+++ b/.changeset/maps-maplibre-styles.md
@@ -0,0 +1,7 @@
+---
+"@tabler/core": minor
+"@tabler/preview": patch
+"@tabler/docs": patch
+---
+
+Added MapLibre GL styles to `tabler-vendors.css`: map controls, popups and attribution, in light and dark mode.
diff --git a/core/package.json b/core/package.json
index 555bdbf1d..e5d718d4f 100644
--- a/core/package.json
+++ b/core/package.json
@@ -196,19 +196,19 @@
},
{
"path": "./dist/css/tabler-vendors.css",
- "maxSize": "4 kB"
+ "maxSize": "4.5 kB"
},
{
"path": "./dist/css/tabler-vendors.min.css",
- "maxSize": "3.5 kB"
- },
- {
- "path": "./dist/css/tabler-vendors.rtl.css",
"maxSize": "4 kB"
},
+ {
+ "path": "./dist/css/tabler-vendors.rtl.css",
+ "maxSize": "4.5 kB"
+ },
{
"path": "./dist/css/tabler-vendors.rtl.min.css",
- "maxSize": "3.5 kB"
+ "maxSize": "4 kB"
},
{
"path": "./dist/js/tabler.js",
diff --git a/core/scss/tabler-vendors.scss b/core/scss/tabler-vendors.scss
index cce656e31..8633982ba 100644
--- a/core/scss/tabler-vendors.scss
+++ b/core/scss/tabler-vendors.scss
@@ -4,6 +4,7 @@
@forward 'vendor/tom-select';
@forward 'vendor/apexcharts';
@forward 'vendor/jsvectormap';
+@forward 'vendor/maplibre';
@forward 'vendor/dropzone';
@forward 'vendor/fslightbox';
@forward 'vendor/plyr';
diff --git a/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap b/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap
index cc24d90a3..88e717e1b 100644
--- a/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap
+++ b/core/scss/tests/__snapshots__/css-var-prefix.test.mjs.snap
@@ -90,6 +90,7 @@ exports[`css custom-property prefixing > leaves vendor-owned names alone and pre
"--tblr-secondary",
"--tblr-secondary-bg",
"--tblr-shadow-card",
+ "--tblr-shadow-color",
"--tblr-shadow-dropdown",
"--tblr-shadow-input",
"--tblr-signal-color",
diff --git a/core/scss/vendor/_maplibre.scss b/core/scss/vendor/_maplibre.scss
new file mode 100644
index 000000000..562d9fc08
--- /dev/null
+++ b/core/scss/vendor/_maplibre.scss
@@ -0,0 +1,133 @@
+@use '../config' as *;
+
+.maplibregl-map {
+ font-family: inherit;
+ font-size: $h5-font-size;
+ line-height: $line-height-base;
+}
+
+// Controls: zoom, compass, fullscreen, locate
+.maplibregl-ctrl-group {
+ overflow: hidden;
+ background: var(--bg-surface);
+ @include border-radius(var(--border-radius));
+
+ &:not(:empty) {
+ box-shadow:
+ 0 0 0 var(--border-width) var(--border-color),
+ #{$card-box-shadow};
+ }
+
+ button {
+ width: 2rem;
+ height: 2rem;
+
+ + button {
+ border-top: var(--border-width) var(--border-style) var(--border-color);
+ }
+
+ &:focus:focus-visible {
+ outline-offset: calc(-1 * var(--focus-ring-width));
+ box-shadow: none;
+ @include focus-ring();
+ }
+ }
+}
+
+.maplibregl-ctrl button:not(:disabled):hover,
+.maplibregl-ctrl button:not(:disabled):active {
+ background-color: var(--bg-surface-secondary);
+}
+
+// Attribution
+.maplibregl-ctrl.maplibregl-ctrl-attrib {
+ color: var(--secondary);
+ background-color: color-transparent(var(--bg-surface), 0.7);
+
+ a {
+ color: inherit;
+ }
+}
+
+.maplibregl-ctrl-attrib.maplibregl-compact {
+ color: var(--secondary);
+ background-color: var(--bg-surface);
+}
+
+.maplibregl-ctrl-attrib-button:focus {
+ box-shadow: none;
+ @include focus-ring();
+}
+
+.maplibregl-ctrl-scale {
+ color: var(--body-color);
+ background-color: color-transparent(var(--bg-surface), 0.75);
+ border-color: var(--body-color);
+}
+
+// Popup
+.maplibregl-popup-content {
+ padding: 0.75rem 1rem;
+ color: var(--body-color);
+ background: var(--bg-surface);
+ box-shadow:
+ 0 0 0 var(--border-width) var(--border-color),
+ #{$box-shadow-md};
+ @include border-radius(var(--border-radius));
+}
+
+// Room for the close button, which MapLibre pins to the right edge in both directions
+.maplibregl-popup-content:has(.maplibregl-popup-close-button) {
+ /* rtl:ignore */
+ padding-right: 2.25rem;
+}
+
+.maplibregl-popup-close-button {
+ width: 1.5rem;
+ height: 1.5rem;
+ padding: 0;
+ margin: 0.375rem;
+ font-size: 1rem;
+ line-height: 1;
+ color: var(--secondary);
+ @include border-radius(var(--border-radius));
+
+ &:hover {
+ color: var(--body-color);
+ background-color: var(--bg-surface-secondary);
+ }
+}
+
+.maplibregl-popup-anchor-top,
+.maplibregl-popup-anchor-top-left,
+.maplibregl-popup-anchor-top-right {
+ .maplibregl-popup-tip {
+ border-bottom-color: var(--bg-surface);
+ }
+}
+
+.maplibregl-popup-anchor-bottom,
+.maplibregl-popup-anchor-bottom-left,
+.maplibregl-popup-anchor-bottom-right {
+ .maplibregl-popup-tip {
+ border-top-color: var(--bg-surface);
+ }
+}
+
+.maplibregl-popup-anchor-left .maplibregl-popup-tip {
+ border-right-color: var(--bg-surface);
+}
+
+.maplibregl-popup-anchor-right .maplibregl-popup-tip {
+ border-left-color: var(--bg-surface);
+}
+
+// The control icons are black SVG images
+@if $enable-dark-mode {
+ @include color-mode(dark) {
+ .maplibregl-ctrl-icon,
+ .maplibregl-ctrl-attrib-button {
+ @include autodark-image();
+ }
+ }
+}
diff --git a/docs/content/ui/components/map.mdx b/docs/content/ui/components/map.mdx
index 69714cae6..be3b2fa86 100644
--- a/docs/content/ui/components/map.mdx
+++ b/docs/content/ui/components/map.mdx
@@ -40,6 +40,13 @@ Or include it from a CDN. You need two files: the stylesheet and the library.
`}
/>
+Tabler styles the map controls, popups and attribution to match the interface, in light and dark mode. These styles are in `tabler-vendors.css`, so load it after the MapLibre stylesheet:
+
+`}
+/>
+
## Usage
### Markup
@@ -115,6 +122,53 @@ cities.forEach(function (coords) {
The `color` option is written to the `fill` attribute of the marker icon. A CSS variable works there, so `var(--tblr-primary)` keeps the markers in your theme color, including after a theme change.
+### Popups
+
+Attach a popup to a marker with `setPopup`. It opens when the marker is clicked. Click a marker on the map above to see one.
+
+```js
+new maplibregl.Marker({ color: 'var(--tblr-primary)' })
+ .setLngLat([16.366667, 48.2])
+ .setPopup(new maplibregl.Popup({ offset: 32 }).setText('Vienna'))
+ .addTo(map)
+```
+
+Use `setHTML` instead of `setText` for richer content. Only pass HTML you trust, as it is not escaped.
+
+### Controls
+
+Add the zoom and compass buttons with `NavigationControl`. They go to the top right corner by default.
+
+```js
+map.addControl(new maplibregl.NavigationControl())
+```
+
+MapLibre also has `FullscreenControl`, `GeolocateControl` and `ScaleControl`. Pass a corner as the second argument, for example `'bottom-left'`.
+
+### Dark mode
+
+A map style does not change with the page theme on its own. Pick the style from the `data-bs-theme` attribute, and set it again when the attribute changes:
+
+```js
+const themeStyle = function () {
+ const dark = document.documentElement.getAttribute('data-bs-theme') === 'dark'
+ return 'https://tiles.openfreemap.org/styles/' + (dark ? 'dark' : 'liberty')
+}
+
+const map = new maplibregl.Map({
+ container: 'map-simple',
+ style: themeStyle(),
+ zoom: 13,
+ center: [13.4049, 52.518827],
+})
+
+new MutationObserver(function () {
+ map.setStyle(themeStyle())
+}).observe(document.documentElement, { attributeFilter: ['data-bs-theme'] })
+```
+
+Markers and popups stay on the map when the style changes. The maps on this page follow the theme this way.
+
### Attribution
MapLibre adds the credit for the map data in the corner of the map. OpenFreeMap requires it, so do not hide it.
diff --git a/preview/pages/map-fullsize.astro b/preview/pages/map-fullsize.astro
index f1b00c114..bc2ee098f 100644
--- a/preview/pages/map-fullsize.astro
+++ b/preview/pages/map-fullsize.astro
@@ -12,15 +12,20 @@ import CaptureScript from '@shared/components/CaptureScript.astro'
window.tabler_map ??= {}
function initMap() {
+ const themeStyle = () => `https://tiles.openfreemap.org/styles/${document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'liberty'}`
+
const map = new maplibregl.Map({
container: 'map-fullsize',
- style: 'https://tiles.openfreemap.org/styles/liberty',
+ style: themeStyle(),
zoom: 12,
center: [13.4049, 52.518827],
})
map.addControl(new maplibregl.NavigationControl())
+ // Change the map style when the page theme changes
+ new MutationObserver(() => map.setStyle(themeStyle())).observe(document.documentElement, { attributeFilter: ['data-bs-theme'] })
+
window.tabler_map['map-fullsize'] = map
}
diff --git a/shared/components/demo/Map.astro b/shared/components/demo/Map.astro
index 4363946ab..1637130bf 100644
--- a/shared/components/demo/Map.astro
+++ b/shared/components/demo/Map.astro
@@ -1,5 +1,6 @@
---
// MapLibre GL map on OpenFreeMap tiles: no account and no access token.
+// The map follows the page theme: `style` in light mode, `darkStyle` in dark.
// Renders nothing unless maps[mapId] exists.
import maps from '@data/maps.json';
import CaptureScript from '../CaptureScript.astro';
@@ -18,6 +19,10 @@ type MapData = {
card?: boolean;
/** OpenFreeMap style name: liberty (default), bright, positron, dark or fiord */
style?: string;
+ /** style used in dark mode, `dark` by default */
+ darkStyle?: string;
+ /** adds the zoom and compass buttons */
+ controls?: boolean;
zoom?: number;
center?: [number, number];
markers?: { name?: string; center: [number, number] }[];
@@ -26,10 +31,13 @@ type MapData = {
const data = (maps as unknown as Record)[mapId];
const mapContainerId = `map-${mapId}`;
-const style = `https://tiles.openfreemap.org/styles/${data?.style ?? 'liberty'}`;
+const styleUrl = (name: string) => `https://tiles.openfreemap.org/styles/${name}`;
+const style = styleUrl(data?.style ?? 'liberty');
+const darkStyle = styleUrl(data?.darkStyle ?? 'dark');
+const controls = data?.controls ?? false;
const zoom = data?.zoom ?? 13;
const center = data?.center ? [data.center[1], data.center[0]] : [13.4049, 52.518827];
-const markers = (data?.markers ?? []).map((marker) => [marker.center[1], marker.center[0]]);
+const markers = (data?.markers ?? []).map((marker) => ({ name: marker.name, coords: [marker.center[1], marker.center[0]] }));
const ratioClass = data?.ratio ?? ratio ?? '16x9';
---
@@ -47,21 +55,33 @@ const ratioClass = data?.ratio ?? ratio ?? '16x9';
data && (
-