---
title: Context Menu
source: https://vjmap.com/en/guide/contextMenu.html
---

# Context Menu

`ContextMenu` is a context menu that displays a right-click menu interface.

## Common Methods

|Method|Description|
|:---|:---|
|add|Add menu item|
|show|Show menu |
|hide|Hide menu|
|toggle|Toggle menu visibility|

## Menu Item Types
|Type|Description|
|:---|:---|
|seperator|Separator line|
|custom|Custom menu item|
|multi|Multiple buttons|
|submenu|Submenu item|
|hovermenu|Hover menu item|
|normal|Default normal menu item|

## Examples

### Map Context Menu 

[Map context menu](https://vjmap.com/demo/#/demo/map/contextmenu/01contextmenuMap)
```js
// Selected icon, or specify src path directly
const checkIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAANpJREFUOE9jZKAQMFKon2FgDKjZHVHf4rqiEeR6kl1QuzOyn+E/4/lmj2WLSDagekdkASMjg1aL+/I0WNgR7YLaHRG2/xgZYlrdV6QjBzzRBtTsijzzl4nVvd1l0VucBtTsjlz5/z/jt1a3ZYnIimp2RZ7//+9fQ6vHyo3o0Q53Qc32CAcGpv88DIxMxQyMDB9aXJcHghTX7IxcyMjAeLTZfdksbGkGqxdqdkUuZ2BgkPjPyDCFgeG/W6srqr+JCoPqXZG9jAwM9iyszy0aHA/8wZViiQ7EYWwAAPShQBF/1IlVAAAAAElFTkSuQmCC";
map.setMenu(event => {
    return new vjmap.ContextMenu({
        event: event.originalEvent,
        theme: "dark", //light
        width: "250px",
        items: [
            {type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">Context menu demo</span>`},
            {type: 'multi', items: [
                    {label: 'Fit to map', onClick: () => { map.fitMapBounds(); }},
                    {label: 'Reset rotation', onClick: () => { map.setBearing(0); }},
                    {label: 'Reset pitch', onClick: () => { map.setPitch(0); }},
                ]},
            {label: 'Get coordinates at this position', onClick: () => {
                    let point = map.fromLngLat(event.lngLat);
                    map.logInfo(`Current coordinates: x: ${point.x}, y: ${point.y}`);
                }},
            {type: 'seperator'},
            {type: 'submenu', label: 'Map zoom', items: [
                    {label: 'Zoom in one level', onClick: () => { map.setZoom(map.getZoom() + 1)}},
                    {label: 'Zoom out one level', onClick: () => { map.setZoom(map.getZoom() - 1)}, enabled: map.getZoom() - 1 > 0},
                    {label: 'Zoom to minimum level', onClick: () => { map.setZoom(0)}},
                    {label: 'Fly to this position', onClick: () => {
                            map.flyTo({
                                center: event.lngLat,
                                pitch: 60,
                                zoom: 5
                            })
                        }}
                ]},
            {type: 'hovermenu', label: 'Map settings', items: [
                    {
                        label: 'Double-click to zoom',
                        icon: map.doubleClickZoom.isEnabled() ? checkIcon : "",
                        onClick: () => {
                            if (map.doubleClickZoom.isEnabled()) {
                                map.doubleClickZoom.disable()
                            } else {
                                map.doubleClickZoom.enable()
                            }
                        }
                    },
                    {
                        label: 'Allow map rotation',
                        icon: map.dragRotate.isEnabled() ? checkIcon : "",
                        onClick: () => {
                            if (map.dragRotate.isEnabled()) {
                                map.dragRotate.disable();
                            } else {
                                map.dragRotate.enable();
                            }
                        }
                    },
                    {
                        label: 'Allow map pitch',
                        icon: map.getMaxPitch() != 0 ? checkIcon : "",
                        onClick: () => {
                            if (map.getMaxPitch() == 0) {
                                map.setMaxPitch(85)
                            } else {
                                map.setMaxPitch(0)
                            }
                        }
                    }
                ]}
        ]
    });
})
```

<mapFrameEx>
<pre>
// --Map context menu--
// Map service object
let svc = new vjmap.Service(env.serviceUrl, env.accessToken)
// Open map
let res = await svc.openMap({
    mapid: env.exampleMapId, // Map ID
    mapopenway: vjmap.MapOpenWay.GeomRender, // Open with geometric data rendering
    style: vjmap.openMapDarkStyle() // Use dark background style when div has dark background
})
if (res.error) {
    // If open fails
    message.error(res.error)
}
// Create geometric projection coordinate system based on map extent
let prj = new vjmap.GeoProjection(res.bounds);
// Map object
let map = new vjmap.Map({
    container: 'map', // DIV container ID
    style: svc.rasterStyle(), // Style, raster style here
    center: prj.toLngLat(prj.getMapExtent().center()), // Set map center
    zoom: 2, // Set zoom level
    renderWorldCopies: false
});
// Associate service and projection objects
map.attach(svc, prj);
let mapBounds = map.getGeoBounds(); // Get map geographic extent
// Selected icon, or specify src path directly
const checkIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAANpJREFUOE9jZKAQMFKon2FgDKjZHVHf4rqiEeR6kl1QuzOyn+E/4/lmj2WLSDagekdkASMjg1aL+/I0WNgR7YLaHRG2/xgZYlrdV6QjBzzRBtTsijzzl4nVvd1l0VucBtTsjlz5/z/jt1a3ZYnIimp2RZ7//+9fQ6vHyo3o0Q53Qc32CAcGpv88DIxMxQyMDB9aXJcHghTX7IxcyMjAeLTZfdksbGkGqxdqdkUuZ2BgkPjPyDCFgeG/W6srqr+JCoPqXZG9jAwM9iyszy0aHA/8wZViiQ7EYWwAAPShQBF/1IlVAAAAAElFTkSuQmCC";
map.setMenu(event =&gt; {
    return new vjmap.ContextMenu({
        event: event.originalEvent,
        theme: "dark", //light
        width: "250px",
        items: [
            {type: 'custom', markup: `&lt;span style="color: #ffff00; padding-left: 30px"&gt;Context menu demo&lt;/span&gt;`},
            {type: 'multi', items: [
                    {label: 'Fit to map', onClick: () =&gt; { map.fitMapBounds(); }},
                    {label: 'Reset rotation', onClick: () =&gt; { map.setBearing(0); }},
                    {label: 'Reset pitch', onClick: () =&gt; { map.setPitch(0); }},
                ]},
            {label: 'Get coordinates at this position', onClick: () =&gt; {
                    let point = map.fromLngLat(event.lngLat);
                    map.logInfo(`Current coordinates: x: ${point.x}, y: ${point.y}`);
                }},
            {type: 'seperator'},
            {type: 'submenu', label: 'Map zoom', items: [
                    {label: 'Zoom in one level', onClick: () =&gt; { map.setZoom(map.getZoom() + 1)}},
                    {label: 'Zoom out one level', onClick: () =&gt; { map.setZoom(map.getZoom() - 1)}, enabled: map.getZoom() - 1 &gt; 0},
                    {label: 'Zoom to minimum level', onClick: () =&gt; { map.setZoom(0)}},
                    {label: 'Fly to this position', onClick: () =&gt; {
                            map.flyTo({
                                center: event.lngLat,
                                pitch: 60,
                                zoom: 5
                            })
                        }}
                ]},
            {type: 'hovermenu', label: 'Map settings', items: [
                    {
                        label: 'Double-click to zoom',
                        icon: map.doubleClickZoom.isEnabled() ? checkIcon : "",
                        onClick: () =&gt; {
                            if (map.doubleClickZoom.isEnabled()) {
                                map.doubleClickZoom.disable()
                            } else {
                                map.doubleClickZoom.enable()
                            }
                        }
                    },
                    {
                        label: 'Allow map rotation',
                        icon: map.dragRotate.isEnabled() ? checkIcon : "",
                        onClick: () =&gt; {
                            if (map.dragRotate.isEnabled()) {
                                map.dragRotate.disable();
                            } else {
                                map.dragRotate.enable();
                            }
                        }
                    },
                    {
                        label: 'Allow map pitch',
                        icon: map.getMaxPitch() != 0 ? checkIcon : "",
                        onClick: () =&gt; {
                            if (map.getMaxPitch() == 0) {
                                map.setMaxPitch(85)
                            } else {
                                map.setMaxPitch(0)
                            }
                        }
                    }
                ]}
        ]
    });
})
map.logInfo('Right-click to open the menu');
</pre>
</mapFrameEx>
<br/>
<br/>


### Overlay Layer Context Menu


[Overlay layer context menu](https://vjmap.com/demo/#/demo/map/contextmenu/02contextmenuOverlay)

```js
// Show menu when clicking an overlay. The key can be customized. If the function returns null, the map context menu above is triggered. If the function returns non-null, this menu is shown
map.setMenu(event => {
    let features = map.queryRenderedFeatures(event.point, {
        layers: [polygon.layerId]
    });
    if (features.length == 0) {
        // If the click position has no features from the polygon layer above, return null to trigger the default map menu
        return null;
    }
    // If the click position has features from the polygon layer, show this layer's custom context menu
    return new vjmap.ContextMenu({
        event: event.originalEvent,
        theme: "dark", //light
        width: "250px",
        items: [
            {type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">I am the polygon layer context menu</span>`},
            {
                label: 'View selected polygon info',
                onClick: () => {
                    map.logInfo(`Current polygon ID: ${features[0].properties.id}, Current polygon color: ${features[0].properties.color}`);
                }
            },
            {
                label: 'Delete',
                onClick: () => {
                    let data = polygon.getData();
                    data.features = data.features.filter(f => f.id != features[0].properties.id);
                    polygon.setData(data);
                }
            }
        ]
    });
}, "overlay")
```

<mapFrameEx>
<pre>
// --Overlay layer context menu--
// Map service object
let svc = new vjmap.Service(env.serviceUrl, env.accessToken)
// Open map
let res = await svc.openMap({
    mapid: env.exampleMapId, // Map ID
    mapopenway: vjmap.MapOpenWay.GeomRender, // Open with geometric data rendering
    style: vjmap.openMapDarkStyle() // Use dark background style when div has dark background
})
if (res.error) {
    // If open fails
    message.error(res.error)
}
// Create geometric projection coordinate system based on map extent
let prj = new vjmap.GeoProjection(res.bounds);
// Map object
let map = new vjmap.Map({
    container: 'map', // DIV container ID
    style: svc.rasterStyle(), // Style, raster style here
    center: prj.toLngLat(prj.getMapExtent().center()), // Set map center
    zoom: 2, // Set zoom level
    renderWorldCopies: false
});
// Associate service and projection objects
map.attach(svc, prj);
await map.onLoad();
// Add many polygons for testing. Right-click on a polygon to show polygon context menu, otherwise show map context menu
let mapBounds = map.getGeoBounds(0.6);
let len = mapBounds.width() / 50;
let polygons = []
for(let i = 0; i &lt; 200; i++) {
    const p1 = mapBounds.randomPoint();
    const p2 = vjmap.geoPoint([p1.x, p1.y + len]);
    const p3 = vjmap.geoPoint([p1.x + len, p1.y]);
    polygons.push({
        points: map.toLngLat([p1, p2, p3]),
        properties: {
            name: "polygon" + (i + 1),
            color: vjmap.randomColor()
        }
    })
}
let polygon = new vjmap.Polygon({
    data: polygons,
    // When hover, use red; otherwise use 'color' from properties
    fillColor: ['case', ['to-boolean', ['feature-state', 'hover']], 'red', ['get', 'color']],
    fillOpacity: 0.8,
    fillOutlineColor: "#f00",
    isHoverPointer: true,
    isHoverFeatureState: true
});
polygon.addTo(map);
polygon.clickLayer(e =&gt; map.logInfo(`You clicked polygon ${e.features[0].id}, name: ${e.features[0].properties.name}, color: ${e.features[0].properties.color}`))
// By default the map menu is shown because the key here is empty. When setMenu is called multiple times, order is by key descending; empty key is last
map.setMenu(event =&gt; {
    return new vjmap.ContextMenu({
        event: event.originalEvent,
        theme: "dark", //light
        width: "250px",
        items: [
            {type: 'custom', markup: `&lt;span style="color: #ffff00; padding-left: 30px"&gt;I am the map context menu&lt;/span&gt;`},
            {
                label: 'Fit to map extent',
                onClick: () =&gt; {
                    map.setBearing(0);
                    map.setPitch(0);
                    map.fitMapBounds();
                }
            },
            {
                label: 'Get coordinates at this position',
                onClick: () =&gt; {
                    let point = map.fromLngLat(event.lngLat);
                    map.logInfo(`Current coordinates: x: ${point.x}, y: ${point.y}`);
                }
            }
        ]
    });
})
// Show menu when clicking an overlay. The key can be customized. If the function returns null, the map context menu above is triggered. If the function returns non-null, this menu is shown
map.setMenu(event =&gt; {
    let features = map.queryRenderedFeatures(event.point, {
        layers: [polygon.layerId]
    });
    if (features.length == 0) {
        // If the click position has no features from the polygon layer above, return null to trigger the default map menu
        return null;
    }
    // If the click position has features from the polygon layer, show this layer's custom context menu
    return new vjmap.ContextMenu({
        event: event.originalEvent,
        theme: "dark", //light
        width: "250px",
        items: [
            {type: 'custom', markup: `&lt;span style="color: #ffff00; padding-left: 30px"&gt;I am the polygon layer context menu&lt;/span&gt;`},
            {
                label: 'View selected polygon info',
                onClick: () =&gt; {
                    map.logInfo(`Current polygon ID: ${features[0].properties.id}, Current polygon color: ${features[0].properties.color}`);
                }
            },
            {
                label: 'Delete',
                onClick: () =&gt; {
                    let data = polygon.getData();
                    data.features = data.features.filter(f =&gt; f.id != features[0].properties.id);
                    polygon.setData(data);
                }
            }
        ]
    });
}, "overlay")
// Draw control context menu
const draw = new vjmap.Draw.Tool();
map.addControl(draw, 'top-right');
map.on("draw.simpleselect.contextmenu", function (e) {
    // If not an event from this draw object, ignore
    if (e.styleId != draw.options.styleId) return;
    let feature = e.event.featureTarget
    if (!feature) return
    //if (draw.getMode() != "simple_select") return
    let selectFeatureId = feature.properties.id
    // Must be selected to right-click
    let selectIds = draw.getSelectedIds() || []
    if (selectIds.indexOf(selectFeatureId)== -1) return
    let event = e.event.originalEvent
    // Prevent default map context menu
    event.preventDefault();
    event.stopPropagation();
    return new vjmap.ContextMenu({
        event: event,
        theme: "dark", //light
        width: "250px",
        items: [
            {
                label: "Delete",
                onClick: () =&gt; {
                    draw.delete(selectFeatureId)
                }
            }
        ]
    });
});
</pre>
</mapFrameEx>
<br/>
<br/>

### Marker Context Menu

[Marker context menu](https://vjmap.com/demo/#/demo/map/contextmenu/03contextmenuMarker)
```js
 // Unlike polygon and other overlay layers, marker is a div while overlay layers are canvas-rendered and share the same div as the map
// So marker right-click must be triggered by its own div's context menu
marker.getElement().addEventListener("mousedown", event => {
    if (event.button != 2) return;// Not right-click
    // Prevent default map context menu
    event.preventDefault();
    event.stopPropagation();
    return new vjmap.ContextMenu({
        event: event,
        theme: "dark", //light
        width: "250px",
        items: [
            {type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">I am the Marker context menu</span>`},
            {
                label: 'Get this Marker info',
                onClick: () => {
                    map.logInfo(`Current Marker ID: ${marker.id}`);
                }
            }
        ]
    });
})
```

<mapFrameEx>
<pre>
// --Marker context menu--
// Map service object
let svc = new vjmap.Service(env.serviceUrl, env.accessToken)
// Open map
let res = await svc.openMap({
    mapid: env.exampleMapId, // Map ID
    mapopenway: vjmap.MapOpenWay.GeomRender, // Open with geometric data rendering
    style: vjmap.openMapDarkStyle() // Use dark background style when div has dark background
})
if (res.error) {
    // If open fails
    message.error(res.error)
}
// Create geometric projection coordinate system based on map extent
let prj = new vjmap.GeoProjection(res.bounds);
// Map object
let map = new vjmap.Map({
    container: 'map', // DIV container ID
    style: svc.rasterStyle(), // Style, raster style here
    center: prj.toLngLat(prj.getMapExtent().center()), // Set map center
    zoom: 2, // Set zoom level
    renderWorldCopies: false
});
// Associate service and projection objects
map.attach(svc, prj);
await map.onLoad();
map.setMenu(event =&gt; {
    return new vjmap.ContextMenu({
        event: event.originalEvent,
        theme: "dark", //light
        width: "250px",
        items: [
            {type: 'custom', markup: `&lt;span style="color: #ffff00; padding-left: 30px"&gt;I am the map context menu&lt;/span&gt;`},
            {
                label: 'Fit to map extent',
                onClick: () =&gt; {
                    map.setBearing(0);
                    map.setPitch(0);
                    map.fitMapBounds();
                }
            },
            {
                label: 'Get coordinates at this position',
                onClick: () =&gt; {
                    let point = map.fromLngLat(event.lngLat);
                    app.logInfo(`Current coordinates: x: ${point.x}, y: ${point.y}`);
                }
            }
        ]
    });
})
let mapBounds = map.getGeoBounds(); // Get map geographic extent
for(let i = 0; i &lt; 50; i++) {
    let point = mapBounds.randomPoint(); // Random point within geographic extent
    // marker
    let marker = new vjmap.Marker({
        color: vjmap.randomColor()
    });
    marker.setLngLat(map.toLngLat(point))
    marker.addTo(map);
    marker.id = i + 1;
    // Unlike polygon and other overlay layers, marker is a div while overlay layers are canvas-rendered and share the same div as the map
    // So marker right-click must be triggered by its own div's context menu
    marker.getElement().addEventListener("mousedown", event =&gt; {
        if (event.button != 2) return;// Not right-click
        // Prevent default map context menu
        event.preventDefault();
        event.stopPropagation();
        return new vjmap.ContextMenu({
            event: event,
            theme: "dark", //light
            width: "250px",
            items: [
                {type: 'custom', markup: `&lt;span style="color: #ffff00; padding-left: 30px"&gt;I am the Marker context menu&lt;/span&gt;`},
                {
                    label: 'Get this Marker info',
                    onClick: () =&gt; {
                        map.logInfo(`Current Marker ID: ${marker.id}`);
                    }
                }
            ]
        });
    })
}
</pre>
</mapFrameEx>
<br/>
<br/>


# Type Definitions

```ts
/**
 * Context menu.
 *
 **/
export  class ContextMenu {
    private options;
    private menuControl;
    private position;
    /**
     * Creates a new ContextMenu menu
     * @param {object} opts options which build the menu e.g. position and items
     * @param {number} opts.width sets the width of the menu including children
     * @param {object} opts.event Event object
     * @param {theme} opts.theme Custom theme, supports dark and light, default dark
     * @param {boolean} opts.isSticky sets how the menu appears, follow the mouse or sticky
     * @param {Array<ContextMenuItem>} opts.items sets the default items in the menu
     */
    constructor(opts: ContextMenuOptions);
    /**
     * Adds item to this ContextMenu menu instance
     * @param {ContextMenuItem} item item to add to the ContextMenu menu
     */
    add(item: any): void;
    /**
     * Makes this ContextMenu menu visible
     */
    show(): void;
    /**
     * Hides this ContextMenu menu
     */
    hide(): void;
    /**
     * Toggle visibility of menu
     */
    toggle(): void;
}

/**
 * Context menu options.
 *
 **/
export  interface ContextMenuOptions {
    /** Event object. */
    event: Event;
    /** Menu width (including submenus), pixels, default 150px. */
    width?: string;
    /** Menu theme. (dark and light, default dark)*/
    theme?: string;
    /** Inner width of the menu outer container, used to auto-adjust position when menu exceeds bounds; default is window width. Can be set via map.getContainer().getBoundingClientRect().width */
    innerWidth?: number;
    /** Inner height of the menu outer container, used to auto-adjust position when menu exceeds bounds; default is window height. Can be set via map.getContainer().getBoundingClientRect().height */
    innerHeight?: number;
    /** Menu items */
    items: ContextMenuSubItemOptions[];
}

/**
 * Context menu sub-item options.
 *
 **/
export  interface ContextMenuSubItemOptions {
    /** Submenu type. */
    type?: "custom" | "multi" | "Button" | "seperator" | "submenu" | "hovermenu" | "normal";
    /** Custom HTML content when type is 'custom' */
    markup?: string;
    /** Submenu items when type is 'multi'*/
    items?: ContextMenuSubItemOptions[];
    /** Click handler*/
    onClick?: Function;
    /** Menu label*/
    label?: string;
    /** Shortcut key*/
    shortcut?: string;
    /** Whether enabled*/
    enabled?: boolean;
    /** CSS icon*/
    cssIcon?: string;
    /** Icon*/
    icon?: string;
}

```
