| name | cesiumjs-interaction |
| description | CesiumJS interaction and picking - ScreenSpaceEventHandler, multi-key KeyboardEventModifier input actions, Scene.pick, Scene.drillPick, Scene.pickPosition, mouse and touch events. Use when handling user clicks on the globe, selecting entities or 3D Tiles features, registering modifier-key shortcuts, implementing hover effects, or building drag-based interactions. |
CesiumJS Interaction & Picking
Version baseline: CesiumJS v1.142 (ES module imports, Ion token required).
ScreenSpaceEventHandler
Central class for mouse, touch, and pointer events on the Cesium canvas.
import { ScreenSpaceEventHandler, ScreenSpaceEventType,
KeyboardEventModifier, defined } from "cesium";
let handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction((event) => {
console.log("Clicked at", event.position.x, event.position.y);
}, ScreenSpaceEventType.LEFT_CLICK);
handler.setInputAction((event) => {
console.log("Shift+Click at", event.position);
}, ScreenSpaceEventType.LEFT_CLICK, KeyboardEventModifier.SHIFT);
handler.setInputAction((event) => {
console.log("Ctrl+Shift+Click at", event.position);
}, ScreenSpaceEventType.LEFT_CLICK, [
KeyboardEventModifier.CTRL,
KeyboardEventModifier.SHIFT,
]);
const clickAction = handler.getInputAction(ScreenSpaceEventType.LEFT_CLICK);
const ctrlShiftClickAction = handler.getInputAction(ScreenSpaceEventType.LEFT_CLICK, [
KeyboardEventModifier.SHIFT,
KeyboardEventModifier.CTRL,
]);
if (defined(clickAction) && defined(ctrlShiftClickAction)) {
console.log("Click handlers registered");
}
handler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK);
handler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK, [
KeyboardEventModifier.CTRL,
KeyboardEventModifier.SHIFT,
]);
handler = handler && handler.destroy();
The Viewer also has a built-in handler at viewer.screenSpaceEventHandler -- use it to avoid creating a second handler for simple cases.
ScreenSpaceEventType Reference
| Event | Callback shape | Notes |
|---|
LEFT_DOWN / LEFT_UP / LEFT_CLICK | ({ position }) | Cartesian2 screen coords |
LEFT_DOUBLE_CLICK | ({ position }) | Left only |
RIGHT_DOWN / RIGHT_UP / RIGHT_CLICK | ({ position }) | |
MIDDLE_DOWN / MIDDLE_UP / MIDDLE_CLICK | ({ position }) | |
MOUSE_MOVE | ({ startPosition, endPosition }) | Fires on every pointer move |
WHEEL | (delta) | Positive = scroll up |
PINCH_START | ({ position1, position2 }) | Two-finger touch begins |
PINCH_END | () | Two-finger touch ends |
PINCH_MOVE | ({ distance, angleAndHeight }) | Two-finger move |
KeyboardEventModifier: SHIFT, CTRL, ALT -- optional third argument to
setInputAction, getInputAction, and removeInputAction. In 1.142+, pass a
single modifier or an array of modifiers. Modifier arrays are order-independent
but exact: a handler registered for [CTRL, SHIFT] does not fire when ALT is
also held.
Scene Picking Methods
pick / pickAsync / drillPick / pickPosition
import { Cartographic, Math as CesiumMath, defined } from "cesium";
const picked = viewer.scene.pick(event.position);
const picked2 = await viewer.scene.pickAsync(movement.endPosition);
const allPicked = viewer.scene.drillPick(event.position, 5);
if (viewer.scene.pickPositionSupported) {
const cartesian = viewer.scene.pickPosition(event.position);
if (defined(cartesian)) {
const c = Cartographic.fromCartesian(cartesian);
console.log(CesiumMath.toDegrees(c.longitude), CesiumMath.toDegrees(c.latitude), c.height);
}
}
Set scene.pickTranslucentDepth = true to include translucent primitives in pickPosition.
pickVoxel (experimental)
const voxelCell = viewer.scene.pickVoxel(event.position);
if (defined(voxelCell)) {
console.log(voxelCell.getProperty("temperature"));
}
Picking Return Values
| Picked object | Return shape | Key properties |
|---|
| Entity | { primitive, id } | id is the Entity instance |
| Cesium3DTileFeature | Cesium3DTileFeature | .getProperty(name), .getPropertyIds(), .color |
| Billboard/Label (collection) | { primitive, id } | id is the user-set id |
| Primitive (geometry) | { primitive, id } | id is the GeometryInstance id |
| Globe surface | undefined | Use camera.pickEllipsoid() or pickPosition() |
Recipes
1. Entity Selection with Click
handler.setInputAction((event) => {
const picked = viewer.scene.pick(event.position);
if (defined(picked) && defined(picked.id)) {
viewer.selectedEntity = picked.id;
} else {
viewer.selectedEntity = undefined;
}
}, ScreenSpaceEventType.LEFT_CLICK);
2. 3D Tiles Feature Picking and Property Inspection
import { Cesium3DTileFeature, Color } from "cesium";
handler.setInputAction((event) => {
const picked = viewer.scene.pick(event.position);
if (picked instanceof Cesium3DTileFeature) {
const ids = picked.getPropertyIds();
ids.forEach((id) => console.log(`${id}: ${picked.getProperty(id)}`));
picked.color = Color.YELLOW;
}
}, ScreenSpaceEventType.LEFT_CLICK);
3. Terrain Position Picking (Lon/Lat from Click)
handler.setInputAction((event) => {
const cartesian = viewer.camera.pickEllipsoid(
event.position, viewer.scene.globe.ellipsoid);
if (defined(cartesian)) {
const c = Cartographic.fromCartesian(cartesian);
console.log(`Lon: ${CesiumMath.toDegrees(c.longitude).toFixed(6)}`);
console.log(`Lat: ${CesiumMath.toDegrees(c.latitude).toFixed(6)}`);
}
}, ScreenSpaceEventType.LEFT_CLICK);
For height on 3D content, use scene.pickPosition instead (see above).
4. Multi-Pick with drillPick
import { EntityCollection, CallbackProperty, ColorMaterialProperty, Color } from "cesium";
const pickedEntities = new EntityCollection();
const highlightColor = Color.YELLOW.withAlpha(0.5);
function makePickable(entity, baseColor) {
entity.polygon.material = new ColorMaterialProperty(
new CallbackProperty((time, result) => {
return pickedEntities.contains(entity)
? highlightColor.clone(result) : baseColor.clone(result);
}, false));
}
handler.setInputAction((movement) => {
const all = viewer.scene.drillPick(movement.endPosition);
pickedEntities.removeAll();
for (const p of all) {
if (defined(p.id)) pickedEntities.add(p.id);
}
}, ScreenSpaceEventType.MOUSE_MOVE);
5. Hover Highlighting with MOUSE_MOVE
import { Color } from "cesium";
const highlighted = { feature: undefined, originalColor: new Color() };
handler.setInputAction((movement) => {
if (defined(highlighted.feature)) {
highlighted.feature.color = highlighted.originalColor;
highlighted.feature = undefined;
}
const picked = viewer.scene.pick(movement.endPosition);
if (defined(picked) && defined(picked.color)) {
highlighted.feature = picked;
Color.clone(picked.color, highlighted.originalColor);
picked.color = Color.YELLOW;
}
}, ScreenSpaceEventType.MOUSE_MOVE);
6. Drag-Based Drawing and Measurement
import { Cartographic, EllipsoidGeodesic, Ellipsoid, Color } from "cesium";
const positions = [];
handler.setInputAction((event) => {
const cartesian = viewer.camera.pickEllipsoid(
event.position, viewer.scene.globe.ellipsoid);
if (!defined(cartesian)) return;
positions.push(cartesian);
if (positions.length === 2) {
viewer.entities.add({
polyline: { positions: positions.slice(), width: 3,
material: Color.RED, clampToGround: true },
});
const start = Cartographic.fromCartesian(positions[0]);
const end = Cartographic.fromCartesian(positions[1]);
const geodesic = new EllipsoidGeodesic(start, end, Ellipsoid.WGS84);
console.log(`Distance: ${(geodesic.surfaceDistance / 1000).toFixed(2)} km`);
positions.length = 0;
}
}, ScreenSpaceEventType.LEFT_CLICK);
7. Coordinate Readout on Mouse Move
import { HorizontalOrigin, VerticalOrigin, Cartesian2 } from "cesium";
const coordLabel = viewer.entities.add({
label: { show: false, showBackground: true, font: "14px monospace",
horizontalOrigin: HorizontalOrigin.LEFT, verticalOrigin: VerticalOrigin.TOP,
pixelOffset: new Cartesian2(15, 0) },
});
handler.setInputAction((movement) => {
const cartesian = viewer.camera.pickEllipsoid(
movement.endPosition, viewer.scene.globe.ellipsoid);
if (defined(cartesian)) {
const c = Cartographic.fromCartesian(cartesian);
coordLabel.position = cartesian;
coordLabel.label.show = true;
coordLabel.label.text =
`Lon: ${CesiumMath.toDegrees(c.longitude).toFixed(4)}\n` +
`Lat: ${CesiumMath.toDegrees(c.latitude).toFixed(4)}`;
} else {
coordLabel.label.show = false;
}
}, ScreenSpaceEventType.MOUSE_MOVE);
8. Conditional Behavior Based on Picked Object Type
import { Cesium3DTileFeature } from "cesium";
handler.setInputAction((event) => {
const picked = viewer.scene.pick(event.position);
if (!defined(picked)) {
console.log("No object picked");
} else if (picked instanceof Cesium3DTileFeature) {
console.log("3D Tile feature:", picked.getProperty("name"));
} else if (defined(picked.id) && defined(picked.id.position)) {
viewer.selectedEntity = picked.id;
} else if (defined(picked.primitive)) {
console.log("Primitive:", picked.primitive.constructor.name);
}
}, ScreenSpaceEventType.LEFT_CLICK);
9. pickAsync for Non-Blocking Hover (v1.136+)
const highlighted = { feature: undefined, originalColor: new Color() };
handler.setInputAction(async (movement) => {
if (defined(highlighted.feature)) {
highlighted.feature.color = highlighted.originalColor;
highlighted.feature = undefined;
}
const picked = await viewer.scene.pickAsync(movement.endPosition);
if (defined(picked) && defined(picked.color)) {
highlighted.feature = picked;
Color.clone(picked.color, highlighted.originalColor);
picked.color = Color.YELLOW;
}
}, ScreenSpaceEventType.MOUSE_MOVE);
10. Hover + Selection with Silhouettes (Full Pattern)
import { PostProcessStageLibrary, Color } from "cesium";
const scene = viewer.scene;
const silhouetteHover = PostProcessStageLibrary.createEdgeDetectionStage();
silhouetteHover.uniforms.color = Color.BLUE;
silhouetteHover.uniforms.length = 0.01;
silhouetteHover.selected = [];
const silhouetteSelect = PostProcessStageLibrary.createEdgeDetectionStage();
silhouetteSelect.uniforms.color = Color.LIME;
silhouetteSelect.uniforms.length = 0.01;
silhouetteSelect.selected = [];
scene.postProcessStages.add(
PostProcessStageLibrary.createSilhouetteStage([silhouetteHover, silhouetteSelect]));
let selectedFeature;
viewer.screenSpaceEventHandler.setInputAction((movement) => {
silhouetteHover.selected = [];
const picked = scene.pick(movement.endPosition);
if (defined(picked) && picked !== selectedFeature) {
silhouetteHover.selected = [picked];
}
}, ScreenSpaceEventType.MOUSE_MOVE);
viewer.screenSpaceEventHandler.setInputAction((event) => {
silhouetteSelect.selected = [];
const picked = scene.pick(event.position);
if (defined(picked)) {
selectedFeature = picked;
silhouetteSelect.selected = [picked];
silhouetteHover.selected = [];
} else {
selectedFeature = undefined;
}
}, ScreenSpaceEventType.LEFT_CLICK);
11. Wheel Zoom with Custom Logic
handler.setInputAction((delta) => {
const zoomAmount = delta > 0 ? 0.9 : 1.1;
viewer.camera.zoomIn(viewer.camera.positionCartographic.height * (1 - zoomAmount));
}, ScreenSpaceEventType.WHEEL);
12. Right-Click Context Menu
viewer.scene.canvas.addEventListener("contextmenu", (e) => e.preventDefault());
handler.setInputAction((event) => {
const picked = viewer.scene.pick(event.position);
if (defined(picked) && defined(picked.id)) {
showContextMenu(event.position, picked.id);
}
}, ScreenSpaceEventType.RIGHT_CLICK);
13. Drag Interaction (Move an Entity)
let draggedEntity = null;
const sscc = viewer.scene.screenSpaceCameraController;
handler.setInputAction((event) => {
const picked = viewer.scene.pick(event.position);
if (defined(picked) && defined(picked.id)) {
draggedEntity = picked.id;
sscc.enableRotate = false;
sscc.enableTranslate = false;
}
}, ScreenSpaceEventType.LEFT_DOWN);
handler.setInputAction((movement) => {
if (!defined(draggedEntity)) return;
const cartesian = viewer.camera.pickEllipsoid(
movement.endPosition, viewer.scene.globe.ellipsoid);
if (defined(cartesian)) draggedEntity.position = cartesian;
}, ScreenSpaceEventType.MOUSE_MOVE);
handler.setInputAction(() => {
draggedEntity = null;
sscc.enableRotate = true;
sscc.enableTranslate = true;
}, ScreenSpaceEventType.LEFT_UP);
Performance Tips
- Prefer
pickAsync over pick on MOUSE_MOVE -- synchronous pick stalls the GPU pipeline; pickAsync yields to the GPU and resolves next frame (WebGL2, v1.136+).
- Use
drillPick with a limit -- without one, it re-renders the scene for every overlapping object.
- Avoid
pick in MOUSE_MOVE when only click picking is needed -- MOUSE_MOVE fires on every pointer move and triggers a pick render pass each time.
- Enable
depthTestAgainstTerrain for accurate pickPosition results over terrain.
- Destroy unused handlers -- each one registers DOM listeners that leak memory if not cleaned up.
- Throttle expensive hover logic -- debounce to 50-100ms for operations beyond simple highlighting.
- Check
scene.pickPositionSupported before using pickPosition -- falls back to camera.pickEllipsoid on unsupported GPUs.
- Set
scene.pickTranslucentDepth = true only when needed -- adds an extra render pass.
- Reuse result objects -- pass a scratch
Cartesian3 to pickPosition to avoid GC pressure in MOUSE_MOVE.
- Use
scene.requestRenderMode = true with picking to avoid unnecessary renders; call scene.requestRender() only on state changes.
See Also
- cesiumjs-entities -- Entity API, graphics types, DataSources
- cesiumjs-3d-tiles -- Cesium3DTileset, Cesium3DTileFeature, styling, metadata
- cesiumjs-camera -- Camera.pickEllipsoid, ScreenSpaceCameraController, flyTo