Summary
Enable deck.gl widgets to coexist with native Mapbox/MapLibre controls without DOM overlap by rendering widgets with viewId: 'mapbox' into the map's control container system.
Motivation
Currently, deck widgets and native map controls occupy separate DOM containers:
- Deck widgets: rendered in WidgetManager's overlay container
- Mapbox/MapLibre controls: rendered in
.maplibregl-control-container
When both use the same placement (e.g., top-right), they overlap because they're in completely separate DOM hierarchies. Users have to manually add padding/margins to prevent collision, which is fragile and doesn't adapt to dynamic control changes.
Proposed Solution
Leverage the new _container prop (#9922) to create an IControl adapter pattern:
- Introduce
DeckWidgetControl - a class that wraps a deck widget as a Mapbox IControl
- When
MapboxOverlay receives widgets with viewId: 'mapbox', automatically wrap them as IControls and add to the map
- The IControl's
onAdd() sets widget.props._container to its container element
- WidgetManager appends the widget's DOM into the map's control container
This puts deck widgets into the same DOM hierarchy as native controls - they stack correctly and share positioning.
API
const overlay = new MapboxOverlay({
layers: [...],
widgets: [
// Renders in Mapbox's control container - stacks with native controls
new ZoomWidget({viewId: 'mapbox', placement: 'top-right'}),
new CompassWidget({viewId: 'mapbox', placement: 'top-right'}),
// Renders in deck's overlay container (existing behavior)
new FullscreenWidget({placement: 'top-left'}),
]
});
map.addControl(overlay);
// Native control at same position - they stack correctly now!
map.addControl(new maplibregl.NavigationControl(), 'top-right');
Implementation Details
New: DeckWidgetControl class
export class DeckWidgetControl implements IControl {
private _widget: Widget;
private _container: HTMLDivElement | null = null;
constructor(widget: Widget) {
this._widget = widget;
}
onAdd(map: Map): HTMLElement {
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl mapboxgl-ctrl deck-widget-ctrl';
// Key: WidgetManager will append widget here instead of overlay container
this._widget.props._container = this._container;
return this._container;
}
onRemove(): void {
if (this._widget.props._container === this._container) {
this._widget.props._container = null;
}
this._container?.remove();
this._container = null;
}
getDefaultPosition(): ControlPosition {
const placement = this._widget.placement;
if (!placement || placement === 'fill') {
return 'top-left';
}
return placement;
}
}
Changes to MapboxOverlay
- Add
_widgetControls: DeckWidgetControl[] field
- Add
_processWidgets() method that wraps widgets with viewId: 'mapbox' as IControls
- Call
_processWidgets() before creating Deck instance (so _container is set when WidgetManager initializes)
- Call
_processWidgets() in setProps() when widgets change
- Clean up widget controls in
onRemove()
Why viewId: 'mapbox'?
- Follows existing pattern where
viewId determines which view/container a widget belongs to
- Explicit opt-in - existing widgets continue working unchanged
'mapbox' is descriptive and unlikely to conflict with user-defined view IDs
- Widgets still receive events via WidgetManager (they're passed to Deck regardless of
viewId)
Timing
map.addControl() calls onAdd() synchronously, so the sequence works:
_processWidgets() called with widgets array
- For
viewId: 'mapbox' widgets: map.addControl(control) → onAdd() sets _container
deck.setProps({widgets}) → WidgetManager uses _container
Alternatives Considered
- Manual IControl wrapping by users - Requires users to both wrap widgets AND pass them to overlay, awkward API
- Special prop like
useMapboxControls - Less flexible, all-or-nothing
- Automatic detection based on placement - Too implicit, breaks existing behavior
Breaking Changes
None. Widgets without viewId: 'mapbox' continue to render in deck's overlay container as before.
Related
Summary
Enable deck.gl widgets to coexist with native Mapbox/MapLibre controls without DOM overlap by rendering widgets with
viewId: 'mapbox'into the map's control container system.Motivation
Currently, deck widgets and native map controls occupy separate DOM containers:
.maplibregl-control-containerWhen both use the same placement (e.g.,
top-right), they overlap because they're in completely separate DOM hierarchies. Users have to manually add padding/margins to prevent collision, which is fragile and doesn't adapt to dynamic control changes.Proposed Solution
Leverage the new
_containerprop (#9922) to create an IControl adapter pattern:DeckWidgetControl- a class that wraps a deck widget as a Mapbox IControlMapboxOverlayreceives widgets withviewId: 'mapbox', automatically wrap them as IControls and add to the maponAdd()setswidget.props._containerto its container elementThis puts deck widgets into the same DOM hierarchy as native controls - they stack correctly and share positioning.
API
Implementation Details
New:
DeckWidgetControlclassChanges to
MapboxOverlay_widgetControls: DeckWidgetControl[]field_processWidgets()method that wraps widgets withviewId: 'mapbox'as IControls_processWidgets()before creating Deck instance (so_containeris set when WidgetManager initializes)_processWidgets()insetProps()when widgets changeonRemove()Why
viewId: 'mapbox'?viewIddetermines which view/container a widget belongs to'mapbox'is descriptive and unlikely to conflict with user-defined view IDsviewId)Timing
map.addControl()callsonAdd()synchronously, so the sequence works:_processWidgets()called with widgets arrayviewId: 'mapbox'widgets:map.addControl(control)→onAdd()sets_containerdeck.setProps({widgets})→ WidgetManager uses_containerAlternatives Considered
useMapboxControls- Less flexible, all-or-nothingBreaking Changes
None. Widgets without
viewId: 'mapbox'continue to render in deck's overlay container as before.Related
_containerto WidgetProps #9922 - Added_containerprop to widgets (prerequisite)