mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
538 lines
20 KiB
Plaintext
538 lines
20 KiB
Plaintext
---
|
|
title: Map
|
|
summary: A map shows places and markers on an interactive world map. Tabler provides the markup and the setup code, and the map itself is rendered by MapLibre GL JS.
|
|
description: Show interactive maps with markers using MapLibre GL JS and free map tiles. No account or access token is needed.
|
|
related: [/ui/plugins/vector-map]
|
|
docs-libs: [maplibre-gl]
|
|
source: core/scss/vendor/_maplibre.scss
|
|
css-vars: map-marker
|
|
classnames:
|
|
component:
|
|
- class: ratio
|
|
desc: Wrapper that keeps the map at a fixed aspect ratio
|
|
- class: map-marker
|
|
desc: Marker drawn in HTML, a circle that can hold an icon, a count or an avatar
|
|
part:
|
|
- class: map-marker-label
|
|
desc: Short text under a marker
|
|
modifier:
|
|
- class: map-marker-dot
|
|
desc: Small dot with no content
|
|
- class: map-marker-pin
|
|
desc: Teardrop pin that points down, used with the bottom anchor
|
|
- class: map-popup-card
|
|
desc: Popup with no padding and a fixed width, for an image above the text
|
|
- class: map-tooltip
|
|
desc: Popup shown as a small dark label, for text on hover
|
|
color:
|
|
- class: map-marker-{color}
|
|
desc: Any theme color, for example map-marker-red or map-marker-green
|
|
size:
|
|
- class: ratio-{ratio}
|
|
desc: The aspect to hold, for example ratio-16x9 or ratio-21x9
|
|
---
|
|
import Example from '@components/Example.astro';
|
|
import Map from '@shared/components/demo/Map.astro';
|
|
import MapPalette from '@shared/components/demo/MapPalette.astro';
|
|
import Icon from '@ui/Icon.astro';
|
|
import { Code } from 'astro:components';
|
|
import { site } from '@shared/lib/site.ts';
|
|
|
|
<MapPalette />
|
|
|
|
## Overview
|
|
|
|
The map is a small wrapper around [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/), an open-source map library. Tabler gives you the container markup and a short init script. MapLibre draws the tiles, handles zoom, and renders the markers.
|
|
|
|
The demos load their map tiles from [OpenFreeMap](https://openfreemap.org/). It needs no account and no access token, and it can be used in commercial projects. If you only need a simple map of countries or regions, use the [vector map](/ui/plugins/vector-map) instead. It makes no network calls at all.
|
|
|
|
## Installation
|
|
|
|
MapLibre GL JS is published as an ES module. Tabler uses version 6. Load the stylesheet, then import the library in a module script:
|
|
|
|
<Code
|
|
lang="html"
|
|
code={`<link rel="stylesheet" href="${site.cdnUrl}/dist/libs/maplibre-gl/dist/maplibre-gl.css" />
|
|
<script type="module">
|
|
import * as maplibregl from '${site.cdnUrl}/dist/libs/maplibre-gl/dist/maplibre-gl.mjs'
|
|
|
|
// Makes the library available to the other scripts on the page
|
|
window.maplibregl = maplibregl
|
|
</script>`}
|
|
/>
|
|
|
|
A module script runs before the `DOMContentLoaded` event, so the code examples on this page can use the `maplibregl` global. The library loads two more files from the same folder, `maplibre-gl-shared.mjs` and `maplibre-gl-worker.mjs`, so keep the three together when you host them yourself.
|
|
|
|
With a bundler, install the package and import it in your code:
|
|
|
|
```bash
|
|
npm install maplibre-gl
|
|
```
|
|
|
|
```js
|
|
import * as maplibregl from 'maplibre-gl'
|
|
import 'maplibre-gl/dist/maplibre-gl.css'
|
|
```
|
|
|
|
<Danger title="Use version 6.4.1 or newer">
|
|
Older versions, including all of version 5, have a known security issue in the code that cleans the attribution text.
|
|
</Danger>
|
|
|
|
Browsers do not load ES modules from a page opened straight from the disk. Use a local server to see the maps while you work.
|
|
|
|
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:
|
|
|
|
<Code
|
|
lang="html"
|
|
code={`<link rel="stylesheet" href="${site.cdnUrl}/dist/css/tabler-vendors.min.css" />`}
|
|
/>
|
|
|
|
## Usage
|
|
|
|
### Markup
|
|
|
|
Put the map container inside a `ratio` element, so the map keeps its shape on every screen size. Give the inner element an `id` and the `w-100 h-100` classes.
|
|
|
|
<Example codeOnly>
|
|
<div class="ratio ratio-16x9">
|
|
<div><div id="map-simple" class="w-100 h-100"></div></div>
|
|
</div>
|
|
</Example>
|
|
|
|
Change the shape with the ratio classes, for example `ratio-21x9` for a wide map or `ratio-1x1` for a square one.
|
|
|
|
### Init the map
|
|
|
|
Create the map after the DOM is ready. Pass the id of your container, a style, a zoom level, and a center point.
|
|
|
|
```js
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const map = new maplibregl.Map({
|
|
container: 'map-simple',
|
|
style: 'https://tiles.openfreemap.org/styles/positron',
|
|
zoom: 13,
|
|
center: [13.4049, 52.518827],
|
|
})
|
|
})
|
|
```
|
|
|
|
<Example flush>
|
|
<Map rounded={false} mapId="simple" />
|
|
</Example>
|
|
|
|
`center` takes the longitude first and the latitude second. This is the opposite of what most map services show.
|
|
|
|
### Map styles
|
|
|
|
Set `style` to the URL of any MapLibre style. OpenFreeMap has five of them:
|
|
|
|
| Style | Result |
|
|
| --- | --- |
|
|
| `https://tiles.openfreemap.org/styles/positron` | Light gray map, used by the Tabler demos with [softer colors](#map-colors). Markers and data stand out on it. |
|
|
| `https://tiles.openfreemap.org/styles/dark` | Dark gray map, used by the demos in dark mode. |
|
|
| `https://tiles.openfreemap.org/styles/liberty` | Street map in color. |
|
|
| `https://tiles.openfreemap.org/styles/bright` | Street map with stronger colors. |
|
|
| `https://tiles.openfreemap.org/styles/fiord` | Dark blue map. |
|
|
|
|
<Example flush>
|
|
<Map rounded={false} mapId="light" />
|
|
</Example>
|
|
|
|
### Map colors
|
|
|
|
The Tabler demos make the `positron` and `dark` styles softer: near-white land, pale water and muted labels in light mode, and blue-gray water and roads in dark mode. They do it by changing the paint values of the style layers after the style loads:
|
|
|
|
```js
|
|
map.on('style.load', function () {
|
|
map.setPaintProperty('background', 'background-color', '#fafaf8')
|
|
map.setPaintProperty('water', 'fill-color', '#d4dadc')
|
|
map.setPaintProperty('boundary_2', 'line-color', '#e3cdd0')
|
|
})
|
|
```
|
|
|
|
The full list of colors for both modes is in the `tablerMapPalette()` function in the source of the [maps preview page](https://preview.tabler.io/maps.html). Copy it to get the same look. The layer names belong to the OpenFreeMap styles, so the function works only with them.
|
|
|
|
Styles from other providers work the same way, as long as they follow the MapLibre style format. Check the terms of each provider, as many of them need an API key or a paid plan.
|
|
|
|
### Markers
|
|
|
|
Add a marker for each place. `setLngLat` takes the same longitude and latitude order as `center`. The demos use a small dot, drawn with the `.map-marker` and `.map-marker-dot` classes.
|
|
|
|
```js
|
|
const cities = [
|
|
[-58.666667, -34.58333333], // Buenos Aires
|
|
[16.366667, 48.2], // Vienna
|
|
[116.383333, 39.91666667], // Beijing
|
|
]
|
|
|
|
cities.forEach(function (coords) {
|
|
const element = document.createElement('span')
|
|
element.className = 'map-marker map-marker-dot'
|
|
|
|
new maplibregl.Marker({ element: element }).setLngLat(coords).addTo(map)
|
|
})
|
|
```
|
|
|
|
<Example flush>
|
|
<Map rounded={false} mapId="markers" />
|
|
</Example>
|
|
|
|
Without `element`, MapLibre draws its own pin. Its `color` option accepts a CSS variable, for example `var(--tblr-primary)`.
|
|
|
|
### Custom markers
|
|
|
|
To draw a marker in HTML, create an element with the `.map-marker` class and pass it as `element`. The marker is a circle in the primary color. Put an icon, a short count or an avatar inside it.
|
|
|
|
```js
|
|
const element = document.createElement('span')
|
|
element.className = 'map-marker map-marker-green'
|
|
element.textContent = '12'
|
|
|
|
new maplibregl.Marker({ element: element }).setLngLat([13.35, 52.512]).addTo(map)
|
|
```
|
|
|
|
Put a `.map-marker-label` element inside the marker for a short text under it.
|
|
|
|
Add `.map-marker-dot` for a small dot, or `.map-marker-pin` for a pin that points down. A pin needs `anchor: 'bottom'`, so its tip sits on the place. Change the color with `.map-marker-{color}`.
|
|
|
|
```js
|
|
const pin = document.createElement('span')
|
|
pin.className = 'map-marker map-marker-pin map-marker-red'
|
|
|
|
new maplibregl.Marker({ element: pin, anchor: 'bottom' }).setLngLat([13.47, 52.518]).addTo(map)
|
|
```
|
|
|
|
<Example flush>
|
|
<Map rounded={false} mapId="marker-styles" title="Marker styles" center={[52.522, 13.405]} zoom={11}>
|
|
<div data-lng="13.345" data-lat="52.54"><span class="map-marker map-marker-dot"></span></div>
|
|
<div data-lng="13.39" data-lat="52.545"><span class="map-marker map-marker-dot map-marker-red"></span></div>
|
|
<div data-lng="13.445" data-lat="52.538"><span class="map-marker"><Icon name="building-store" /><span class="map-marker-label">Store</span></span></div>
|
|
<div data-lng="13.35" data-lat="52.512"><span class="map-marker map-marker-green">12</span></div>
|
|
<div data-lng="13.405" data-lat="52.522" data-anchor="bottom"><span class="map-marker map-marker-pin"><Icon name="home" /></span></div>
|
|
<div data-lng="13.47" data-lat="52.518" data-anchor="bottom"><span class="map-marker map-marker-pin map-marker-red"><Icon name="star" /></span></div>
|
|
<div data-lng="13.43" data-lat="52.508"><span class="map-marker"><span class="avatar avatar-sm" style="background-image: url(/static/avatars/000m.jpg)"></span></span></div>
|
|
</Map>
|
|
</Example>
|
|
|
|
You can also write the markers in the page and move them to the map. The preview pages do this: each marker sits in a hidden element with `data-lng` and `data-lat`, and a short loop passes it to MapLibre.
|
|
|
|
```html
|
|
<div id="map-markers" hidden>
|
|
<div data-lng="13.405" data-lat="52.522">
|
|
<span class="map-marker map-marker-pin">...</span>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
```js
|
|
document.querySelectorAll('#map-markers > [data-lng]').forEach(function (item) {
|
|
new maplibregl.Marker({ element: item.firstElementChild, anchor: 'bottom' })
|
|
.setLngLat([Number(item.dataset.lng), Number(item.dataset.lat)])
|
|
.addTo(map)
|
|
})
|
|
```
|
|
|
|
### Draggable markers
|
|
|
|
Pass `draggable: true` to let people move a marker, for example to pick an address. Read the new position in the `dragend` event.
|
|
|
|
```js
|
|
const marker = new maplibregl.Marker({ element: pin, anchor: 'bottom', draggable: true }).setLngLat([13.405, 52.52]).addTo(map)
|
|
|
|
marker.on('dragend', function () {
|
|
console.log(marker.getLngLat())
|
|
})
|
|
```
|
|
|
|
### 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. Tabler draws the popup as a small bordered panel with no arrow.
|
|
|
|
```js
|
|
new maplibregl.Marker({ element: element })
|
|
.setLngLat([16.366667, 48.2])
|
|
.setPopup(new maplibregl.Popup({ offset: 16, closeButton: false }).setText('Vienna'))
|
|
.addTo(map)
|
|
```
|
|
|
|
Leave out `closeButton: false` to show a close button in the corner. The popup also closes on a click on the map.
|
|
|
|
### Tooltips
|
|
|
|
For a short label on hover, add the `map-tooltip` class to a popup. It turns the popup into a small dark label.
|
|
|
|
```js
|
|
const tooltip = new maplibregl.Popup({ closeButton: false, offset: 12, className: 'map-tooltip' })
|
|
|
|
map.on('mousemove', 'cities', function (event) {
|
|
tooltip.setLngLat(event.lngLat).setText(event.features[0].properties.name).addTo(map)
|
|
})
|
|
map.on('mouseleave', 'cities', function () {
|
|
tooltip.remove()
|
|
})
|
|
```
|
|
|
|
Use `setHTML` instead of `setText` for richer content. Only pass HTML you trust, as it is not escaped.
|
|
|
|
### Popup card
|
|
|
|
For a popup with an image, add the `map-popup-card` class with the `className` option. It removes the padding and sets a fixed width, so the image reaches the edges. Keep the content in a `<template>` and pass it with `setDOMContent`.
|
|
|
|
```html
|
|
<template id="popup-museum">
|
|
<div class="img-responsive img-responsive-21x9" style="background-image: url(...)"></div>
|
|
<div class="p-3">
|
|
<div class="fw-medium">Museum Island</div>
|
|
<div class="text-secondary mb-3">Five museums on the Spree, open daily.</div>
|
|
<a href="#" class="btn btn-primary btn-sm">View details</a>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
```js
|
|
const popup = new maplibregl.Popup({ offset: 40, className: 'map-popup-card', maxWidth: 'none' })
|
|
.setDOMContent(document.getElementById('popup-museum').content)
|
|
|
|
new maplibregl.Marker({ element: pin, anchor: 'bottom' }).setLngLat([13.4019, 52.5169]).setPopup(popup).addTo(map)
|
|
```
|
|
|
|
<Example flush>
|
|
<Map rounded={false} mapId="popup-card" title="Popup card" center={[52.546, 13.4]} zoom={11} ratio="4x3">
|
|
<div data-lng="13.4019" data-lat="52.5169" data-anchor="bottom" data-open>
|
|
<span class="map-marker map-marker-pin"><Icon name="map-pin" /></span>
|
|
<template class="map-popup-card">
|
|
<div class="img-responsive img-responsive-21x9" style="background-image: url(/static/photos/a-visit-to-the-bookstore.jpg)"></div>
|
|
<div class="p-3">
|
|
<div class="fw-medium">Museum Island</div>
|
|
<div class="text-secondary mb-3">Five museums on the Spree, open daily.</div>
|
|
<a href="#" class="btn btn-primary btn-sm">View details</a>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</Map>
|
|
</Example>
|
|
|
|
### 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' : 'positron')
|
|
}
|
|
|
|
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.
|
|
|
|
### Layers
|
|
|
|
Lines, areas and large sets of points are drawn as layers. A layer reads its data from a GeoJSON source. Layers are part of the map style, so add them in the `style.load` event. It runs when the map starts and again each time the style changes, for example on a theme change.
|
|
|
|
```js
|
|
map.on('style.load', function () {
|
|
map.addSource('route', {
|
|
type: 'geojson',
|
|
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [[13.3695, 52.5251], [13.4132, 52.5219], [13.4397, 52.505]] } },
|
|
})
|
|
map.addLayer({
|
|
id: 'route',
|
|
type: 'line',
|
|
source: 'route',
|
|
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
|
paint: { 'line-color': '#066fd1', 'line-width': 3, 'line-opacity': 0.8 },
|
|
})
|
|
})
|
|
```
|
|
|
|
For an area, use a `Polygon` geometry with a `fill` layer, and a `line` layer for its outline:
|
|
|
|
```js
|
|
map.addLayer({ id: 'area-fill', type: 'fill', source: 'area', paint: { 'fill-color': '#066fd1', 'fill-opacity': 0.16 } })
|
|
map.addLayer({ id: 'area-outline', type: 'line', source: 'area', paint: { 'line-color': '#066fd1', 'line-width': 2 } })
|
|
```
|
|
|
|
### Theme colors in layers
|
|
|
|
MapLibre draws layers on a canvas, so a CSS variable does not work in `paint`, and neither do the `oklch()` values Tabler colors are written in. Turn the variable into a plain `rgb()` color first:
|
|
|
|
```js
|
|
function mapColor(name) {
|
|
const context = document.createElement('canvas').getContext('2d')
|
|
const probe = document.body.appendChild(document.createElement('span'))
|
|
|
|
probe.style.color = 'var(' + name + ')'
|
|
context.fillStyle = getComputedStyle(probe).color
|
|
context.fillRect(0, 0, 1, 1)
|
|
probe.remove()
|
|
|
|
const pixel = context.getImageData(0, 0, 1, 1).data
|
|
|
|
return 'rgb(' + pixel[0] + ', ' + pixel[1] + ', ' + pixel[2] + ')'
|
|
}
|
|
|
|
map.addLayer({ id: 'route', type: 'line', source: 'route', paint: { 'line-color': mapColor('--tblr-primary'), 'line-width': 3 } })
|
|
```
|
|
|
|
Call it inside `style.load`, so the colors are read again after a theme change.
|
|
|
|
### Clusters
|
|
|
|
HTML markers work well for up to a few hundred places. For more, put the points in a GeoJSON source with `cluster: true`. MapLibre then groups nearby points and splits the groups as you zoom in.
|
|
|
|
```js
|
|
map.addSource('stores', { type: 'geojson', data: stores, cluster: true, clusterRadius: 40 })
|
|
|
|
// Circle for each cluster, larger for more points
|
|
map.addLayer({
|
|
id: 'clusters',
|
|
type: 'circle',
|
|
source: 'stores',
|
|
filter: ['has', 'point_count'],
|
|
paint: { 'circle-color': '#066fd1', 'circle-radius': ['step', ['get', 'point_count'], 14, 10, 18, 25, 24] },
|
|
})
|
|
|
|
// Number of points in the cluster
|
|
map.addLayer({
|
|
id: 'cluster-count',
|
|
type: 'symbol',
|
|
source: 'stores',
|
|
filter: ['has', 'point_count'],
|
|
layout: { 'text-field': ['get', 'point_count_abbreviated'], 'text-font': ['Noto Sans Regular'], 'text-size': 12 },
|
|
paint: { 'text-color': '#ffffff' },
|
|
})
|
|
|
|
// Single points
|
|
map.addLayer({ id: 'store', type: 'circle', source: 'stores', filter: ['!', ['has', 'point_count']], paint: { 'circle-color': '#066fd1', 'circle-radius': 6 } })
|
|
```
|
|
|
|
`text-font` has to be a font the map style provides. All OpenFreeMap styles have `Noto Sans Regular`.
|
|
|
|
### Bubble map
|
|
|
|
To size circles by a value, read it from the feature with `['get', ...]` and map it to a radius with `interpolate`:
|
|
|
|
```js
|
|
map.addLayer({
|
|
id: 'cities',
|
|
type: 'circle',
|
|
source: 'cities',
|
|
paint: {
|
|
'circle-color': '#066fd1',
|
|
'circle-opacity': 0.6,
|
|
'circle-radius': ['interpolate', ['linear'], ['get', 'users'], 0, 4, 1600, 24],
|
|
},
|
|
})
|
|
```
|
|
|
|
### Content over the map
|
|
|
|
To put buttons or a small card over the map, place them next to the map container and position them with the utility classes:
|
|
|
|
```html
|
|
<div class="ratio ratio-16x9">
|
|
<div>
|
|
<div id="map-cities" class="w-100 h-100 rounded"></div>
|
|
<div class="btn-list position-absolute top-0 start-0 m-2">
|
|
<button type="button" class="btn btn-sm btn-pill btn-primary">Berlin</button>
|
|
<button type="button" class="btn btn-sm btn-pill">Tokyo</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### Globe
|
|
|
|
MapLibre can draw the map as a 3D globe. Set the projection in the `style.load` event, as it is part of the map style. People can turn the globe by dragging it.
|
|
|
|
```js
|
|
map.on('style.load', function () {
|
|
map.setProjection({ type: 'globe' })
|
|
})
|
|
```
|
|
|
|
Use a low zoom, between 1 and 2, to see the whole globe. To connect two places with an arc, draw a line layer through points on the shortest path between them. The [maps preview page](https://preview.tabler.io/maps.html) has a short function that returns these points.
|
|
|
|
### Moving the map
|
|
|
|
Move to another place with `flyTo`, for example from a row of buttons:
|
|
|
|
```js
|
|
button.addEventListener('click', function () {
|
|
map.flyTo({ center: [139.6917, 35.6895], zoom: 10 })
|
|
})
|
|
```
|
|
|
|
Live versions of the route, area, clusters, bubble map and city switcher are on the [maps preview page](https://preview.tabler.io/maps.html).
|
|
|
|
### Attribution
|
|
|
|
MapLibre adds the credit for the map data in the corner of the map. OpenFreeMap requires it, so do not hide it.
|
|
|
|
## Examples
|
|
|
|
### Map in a card
|
|
|
|
Add a card title above the map. This is the usual layout for a small map next to other content.
|
|
|
|
<Example codeOnly>
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="card-title">Simple map</div>
|
|
<div class="ratio ratio-16x9">
|
|
<div><div id="map-card" class="w-100 h-100"></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Example>
|
|
|
|
### Full-card map
|
|
|
|
Drop the card body to let the map fill the whole card. Add `rounded` to the container, so the map follows the card corners.
|
|
|
|
<Example codeOnly>
|
|
<div class="card">
|
|
<div class="ratio ratio-21x9">
|
|
<div><div id="map-full" class="w-100 h-100 rounded"></div></div>
|
|
</div>
|
|
</div>
|
|
</Example>
|
|
|
|
Live versions of these maps are on the [maps preview page](https://preview.tabler.io/maps.html).
|
|
|
|
### Blocks
|
|
|
|
The [map blocks preview page](https://preview.tabler.io/map-blocks.html) has larger examples that pair a map with other components: a store locator with a searchable list, a delivery tracker with a route and a status card over the map, a world map of live users, and a vector map of users by country next to a table.
|
|
|
|
## Using Mapbox GL JS
|
|
|
|
MapLibre GL JS started as a fork of Mapbox GL JS, so the two share most of their API. To use Mapbox instead, load its script and stylesheet, replace `maplibregl` with `mapboxgl`, set `mapboxgl.accessToken` to your public token, and use a `mapbox://` style URL.
|
|
|
|
## Accessibility
|
|
|
|
- The map is a picture for most users. Give the same information as text near it, for example the address or a list of places.
|
|
- Add an `aria-label` to the map container, so screen reader users know what the map shows.
|
|
- Do not put an action only on a marker. Repeat it in a normal link or button below the map.
|
|
- Keep a visible fallback when the script fails to load, for example the address in text.
|