overflow: visibleA circular photo popup (a disc with its own ring + drop-shadow) gets square-clipped
by the base .mapboxgl-popup-content rule, which sets overflow: hidden. That clips
the circle's drop-shadow into hard square corners. Override it on a custom class, make
the popup chrome transparent (the disc carries its own ring/shadow), and hide the tip:
.pop-photo .mapboxgl-popup-content {
background: transparent; box-shadow: none; padding: 0; border-radius: 0;
overflow: visible; /* essential — base rule clips the round shadow square */
}
.pop-photo .mapboxgl-popup-tip { display: none; }
.pop-circle {
width: 228px; height: 228px; border-radius: 50%; overflow: hidden;
border: 6px solid #355e3b; box-shadow: 0 6px 22px rgba(0,0,0,.30);
}
.pop-circle img { width: 100%; height: 100%; object-fit: cover; display: block; }
new mapboxgl.Popup({ offset: photoPopupOffset(), closeButton:false,
closeOnClick:true, className:"pop-photo", maxWidth:"none" })
.setLngLat(e.lngLat).setHTML(`<div class="pop-circle"><img src="${url}"></div>`).addTo(map);
If the marker scales with zoom, a fixed popup offset leaves a gap that's wrong at
most zooms. Compute the offset from the current icon-size so the card hugs its
marker at every zoom. Popup offset accepts a per-anchor object so the gap is right
whether the card lands above, below, or beside the marker:
function photoPopupOffset() {
const clear = Math.round(40 * photoIconSize(map.getZoom()) + 6); // tip→top + small gap
return { bottom:[0,-clear], top:[0,14], left:[14,0], right:[-14,0] };
}
Where photoIconSize(z) mirrors the symbol layer's icon-size zoom ramp (interpolate
the same stops in JS), so the popup tracks the rendered marker exactly.
closeButton:false + closeOnClick:true → no X button; click anywhere off the
popup to dismiss (hide the default close button via
.mapboxgl-popup-close-button{display:none} if a stray one appears).Pointer cursor on interactive layers:
for (const lyr of ["clusters","poi-glyph","poi-photo"]) {
map.on("mouseenter", lyr, () => map.getCanvas().style.cursor = "pointer");
map.on("mouseleave", lyr, () => map.getCanvas().style.cursor = "");
}
Animate only opacity (@keyframes on .mapboxgl-popup) — Mapbox owns the
positioning transform, so don't animate transform or the popup jumps.