Popup Card
Runnable MapLibre GL JS example: Popup Card. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
示例:弹出卡片
核心逻辑:点击站点圆点后,用 Popup + setDOMContent 挂一张自绘卡片,offset 避开图标
依赖:MapLibre GL JS 4 · jsDelivr CDN · demotiles 样式
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>弹出卡片</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.css">
<style>
html,body{height:100%;margin:0}
body{display:flex;flex-direction:column;overflow:hidden;
font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif;color:#16181d}
.bar{flex:0 0 auto;display:flex;gap:12px;align-items:center;padding:8px 12px;
background:#f6f7f9;border-bottom:1px solid #e3e6ea;font-size:12px;color:#58606e;flex-wrap:wrap}
.bar b{color:#16181d;font-weight:600}
#map{flex:1 1 auto;min-height:0}
.station-popup .maplibregl-popup-content{
padding:0;border-radius:8px;box-shadow:0 6px 20px rgba(15,30,55,.22);overflow:hidden;
}
.station-popup .maplibregl-popup-tip{border-top-color:#ffffff}
.card-head{background:#1f5fd0;color:#fff;padding:9px 14px;font-weight:600;font-size:13px}
.card-body{padding:10px 14px 13px;color:#28323f}
.card-row{display:flex;justify-content:space-between;gap:18px;font-size:12px;margin-top:4px}
.card-row span:first-child{color:#6b7785}
.card-val{font-size:22px;font-weight:700;color:#16181d;line-height:1.3}
.card-val small{font-size:11px;font-weight:400;color:#6b7785;margin-left:3px}
.card-bar{height:6px;border-radius:3px;background:#e8edf3;margin-top:8px;overflow:hidden}
.card-bar i{display:block;height:100%;border-radius:3px;background:#e8590c}
</style>
</head>
<body>
<div class="bar">
<span>点击圆点查看站点卡片</span>
<span>站点数 <b>6</b></span>
<span id="hint">卡片随地图缩放一起移动</span>
</div>
<div id="map"></div>
<script src="https://cdn.jsdelivr.net/npm/maplibre-gl@4/dist/maplibre-gl.js"></script>
<script>
(function () {
'use strict';
if (typeof maplibregl === 'undefined') {
document.body.innerHTML =
'<div style="padding:24px;color:#b42318">MapLibre 加载失败,请检查能否访问 cdn.jsdelivr.net</div>';
return;
}
var stations = {
type: 'FeatureCollection',
features: [
{ name: 'Binjiang', lng: 120.21, lat: 30.21, value: 82, max: 100, time: '08:00' },
{ name: 'Xihu', lng: 120.13, lat: 30.26, value: 46, max: 100, time: '08:00' },
{ name: 'Xiaoshan', lng: 120.26, lat: 30.16, value: 64, max: 100, time: '07:45' },
{ name: 'Yuhang', lng: 120.30, lat: 30.42, value: 91, max: 100, time: '08:10' },
{ name: 'Fuyang', lng: 119.95, lat: 30.05, value: 37, max: 100, time: '07:50' },
{ name: 'Linan', lng: 119.72, lat: 30.23, value: 58, max: 100, time: '08:05' }
].map(function (s) {
return {
type: 'Feature',
properties: { name: s.name, value: s.value, max: s.max, time: s.time },
geometry: { type: 'Point', coordinates: [s.lng, s.lat] }
};
})
};
var map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [120.0, 30.22],
zoom: 9.2,
attributionControl: { compact: true, customAttribution: 'MapLibre demo tiles' }
});
map.addControl(new maplibregl.NavigationControl(), 'top-right');
function buildCard(props) {
var wrap = document.createElement('div');
var head = document.createElement('div');
head.className = 'card-head';
head.textContent = props.name + ' 观测站';
var body = document.createElement('div');
body.className = 'card-body';
var val = document.createElement('div');
val.className = 'card-val';
val.textContent = props.value;
var unit = document.createElement('small');
unit.textContent = '/ ' + props.max;
val.appendChild(unit);
var bar = document.createElement('div');
bar.className = 'card-bar';
var fill = document.createElement('i');
fill.style.width = (props.value / props.max * 100) + '%';
bar.appendChild(fill);
var row = document.createElement('div');
row.className = 'card-row';
row.innerHTML = '<span>更新时间</span><span>' + props.time + '</span>';
body.appendChild(val);
body.appendChild(bar);
body.appendChild(row);
wrap.appendChild(head);
wrap.appendChild(body);
return wrap;
}
map.on('load', function () {
map.addSource('stations', { type: 'geojson', data: stations });
map.addLayer({
id: 'station-dot',
type: 'circle',
source: 'stations',
paint: {
'circle-radius': 7,
'circle-color': '#e8590c',
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 2.5
}
});
var popup = new maplibregl.Popup({
closeButton: false,
closeOnClick: true,
maxWidth: '300px',
offset: 12,
className: 'station-popup'
});
map.on('click', 'station-dot', function (e) {
var f = e.features[0];
popup
.setLngLat(f.geometry.coordinates)
.setDOMContent(buildCard(f.properties))
.addTo(map);
document.getElementById('hint').textContent =
f.properties.name + ' · 值 ' + f.properties.value;
});
map.on('mouseenter', 'station-dot', function () {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'station-dot', function () {
map.getCanvas().style.cursor = '';
});
});
map.on('error', function (e) {
if (e.error) console.warn('地图错误:' + e.error.message);
});
})();
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.