Skip to content

Select features with a boxZoomEnd callback

Use the boxZoomEnd callback to select features with Shift-drag instead of fitting the map to the dragged box.

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Select features with a boxZoomEnd callback</title>
    <meta property="og:description" content="Use the boxZoomEnd callback to select features with Shift-drag instead of fitting the map to the dragged box." />
    <meta charset='utf-8'>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel='stylesheet' href='https://unpkg.com/maplibre-gl@5.23.0/dist/maplibre-gl.css' />
    <script src='https://unpkg.com/maplibre-gl@5.23.0/dist/maplibre-gl.js'></script>
    <style>
        body { margin: 0; padding: 0; }
        html, body, #map { height: 100%; }
        #ui {
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 1;
            max-width: 360px;
            padding: 8px;
            border-radius: 4px;
            background: rgba(255, 255, 255, 0.9);
            font: 13px/1.4 sans-serif;
        }
    </style>
</head>
<body>
<div id="map"></div>
<div id="ui">
    Shift + drag with <code>boxZoomEnd</code> (no default zoom). Selected: <span id="selected-count">0</span>
    <button id="clear-selection" type="button">Clear</button>
</div>
<script>
    const BASE_LAYER_ID = 'earthquakes-base';
    const SELECTED_LAYER_ID = 'earthquakes-selected';
    const selectedCountElement = document.getElementById('selected-count');

    const map = new maplibregl.Map({
        container: 'map',
        style: 'https://demotiles.maplibre.org/style.json',
        center: [-100, 40],
        zoom: 2.8,
        boxZoom: {
            boxZoomEnd: (mapInstance, p0, p1) => {
                const features = mapInstance.queryRenderedFeatures([
                    [Math.min(p0.x, p1.x), Math.min(p0.y, p1.y)],
                    [Math.max(p0.x, p1.x), Math.max(p0.y, p1.y)]
                ], {layers: [BASE_LAYER_ID]});
                const ids = [...new Set(features.map((feature) => feature.id).filter((id) => id != null))];
                setSelectedIds(ids);
            }
        }
    });

    function setSelectedIds(ids) {
        selectedCountElement.textContent = String(ids.length);
        if (map.getLayer(SELECTED_LAYER_ID)) {
            map.setFilter(SELECTED_LAYER_ID, ['in', ['id'], ['literal', ids]]);
        }
    }

    document.getElementById('clear-selection').addEventListener('click', () => setSelectedIds([]));

    map.on('load', () => {
        map.addSource('earthquakes', {
            type: 'geojson',
            data: 'https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson',
            promoteId: 'id'
        });

        map.addLayer({
            id: BASE_LAYER_ID,
            type: 'circle',
            source: 'earthquakes',
            paint: {'circle-radius': 4, 'circle-color': '#1f78b4', 'circle-opacity': 0.65}
        });
        map.addLayer({
            id: SELECTED_LAYER_ID,
            type: 'circle',
            source: 'earthquakes',
            paint: {'circle-radius': 6, 'circle-color': '#ff6b00', 'circle-opacity': 0.95},
            filter: ['in', ['id'], ['literal', []]]
        });
        setSelectedIds([]);
    });
</script>
</body>
</html>