Skip to main content
Threebox provides built-in interaction capabilities including object selection, dragging, rotation, and raycasting for both 3D objects and fill-extrusion building layers.

Enable Interactions

Configure interaction features when initializing Threebox:
window.tb = new Threebox(
  map,
  mbxContext,
  {
    defaultLights: true,
    enableSelectingObjects: true,      // Click to select 3D objects
    enableSelectingFeatures: true,     // Click to select buildings
    enableDraggingObjects: true,       // Drag objects with [Shift]
    enableRotatingObjects: true,       // Rotate objects with [Alt]
    enableTooltips: true,              // Show tooltips on hover
    enableHelpTooltips: true           // Show help for interactions
  }
);

Interaction Options

OptionTypeDefaultDescription
enableSelectingObjectsbooleanfalseEnable 3D object selection
enableSelectingFeaturesbooleanfalseEnable fill-extrusion selection
enableDraggingObjectsbooleanfalseEnable object dragging
enableRotatingObjectsbooleanfalseEnable object rotation
enableTooltipsbooleanfalseShow default tooltips
enableHelpTooltipsbooleanfalseShow interaction help labels

Object Selection

Click on 3D objects to select them:
map.on('style.load', function() {
  map.addLayer({
    id: 'custom_layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function(map, mbxContext) {
      window.tb = new Threebox(
        map,
        mbxContext,
        {
          defaultLights: true,
          enableSelectingObjects: true,
          enableTooltips: true
        }
      );

      // Create selectable sphere
      var sphere = tb.sphere({
        radius: 30,
        color: 'green',
        material: 'MeshPhysicalMaterial',
        units: 'meters'
      }).setCoords([-122.3512, 47.6202, 0]);

      // Add tooltip
      sphere.addTooltip("Click to select", true);

      // Listen for selection changes
      sphere.addEventListener('SelectedChange', function(e) {
        if (e.detail.selected) {
          console.log('Object selected');
          sphere.setColor('yellow');
        } else {
          console.log('Object unselected');
          sphere.setColor('green');
        }
      }, false);

      tb.add(sphere);
    },

    render: function(gl, matrix) {
      tb.update();
    }
  });
});

Dragging Objects

Drag objects to new positions using keyboard modifiers:
  • [Shift] + Drag - Translate horizontally
  • [Ctrl] + Drag - Adjust altitude
window.tb = new Threebox(
  map,
  mbxContext,
  {
    defaultLights: true,
    enableSelectingObjects: true,
    enableDraggingObjects: true,  // Enable dragging
    enableTooltips: true
  }
);

// Create draggable object
var options = {
  obj: 'models/soldier.glb',
  type: 'gltf',
  scale: 100,
  units: 'meters',
  rotation: { x: 90, y: 0, z: 0 },
  anchor: 'center'
};

tb.loadObj(options, function(model) {
  var soldier = model.setCoords([-122.3491, 47.6207, 0]);
  soldier.addTooltip("Shift+Drag to move, Ctrl+Drag for altitude", true);
  
  // Listen to drag events
  soldier.addEventListener('ObjectDragged', function(e) {
    console.log('Dragged action:', e.detail.draggedAction);
    console.log('New position:', e.detail.draggedObject.coordinates);
  }, false);
  
  tb.add(soldier);
});

Altitude Step

Control altitude adjustment sensitivity:
window.tb = new Threebox(
  map,
  mbxContext,
  {
    enableDraggingObjects: true
  }
);

// Set altitude step (default is 5 meters)
tb.altitudeStep = 1;  // 1 meter increments

Rotating Objects

Rotate objects around their vertical axis:
  • [Alt] + Drag - Rotate on vertical axis
window.tb = new Threebox(
  map,
  mbxContext,
  {
    defaultLights: true,
    enableSelectingObjects: true,
    enableRotatingObjects: true,  // Enable rotation
    enableTooltips: true
  }
);

// Create rotatable tube
var tube = tb.tube({
  geometry: spiralPath,
  radius: 0.8,
  sides: 8,
  color: '#00ffff',
  material: 'MeshPhysicalMaterial'
});
tube.setCoords(origin);
tube.addTooltip("Alt+Drag to rotate", true);
tb.add(tube);

Raycasting

Raycasting enables precise object selection and interaction:
map.on('style.load', function() {
  map.addLayer({
    id: 'custom_layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function(map, mbxContext) {
      window.tb = new Threebox(
        map,
        mbxContext,
        {
          defaultLights: true,
          enableSelectingFeatures: true,   // Buildings
          enableSelectingObjects: true,    // 3D objects
          enableDraggingObjects: true,
          enableRotatingObjects: true,
          enableTooltips: true
        }
      );

      // Create cube without bounding box (still raycasted)
      var geometry = new THREE.BoxGeometry(30, 60, 120);
      var redMaterial = new THREE.MeshPhongMaterial({
        color: 0x660000,
        side: THREE.DoubleSide
      });
      
      var cube = tb.Object3D({
        obj: new THREE.Mesh(geometry, redMaterial),
        units: 'meters',
        bbox: false  // No bounding box, but still selectable
      }).setCoords([-122.3512, 47.6202, 0]);
      
      cube.addTooltip("Selectable without bounding box", true);
      tb.add(cube);

      // Create sphere
      var sphere = tb.sphere({
        radius: 30,
        units: 'meters',
        sides: 120,
        color: 'green',
        material: 'MeshPhysicalMaterial'
      }).setCoords([-122.34548, 47.617538, 0]);
      tb.add(sphere);

      // Load model
      var options = {
        obj: './models/soldier.glb',
        type: 'gltf',
        scale: 100,
        units: 'meters',
        rotation: { x: 90, y: 0, z: 0 },
        anchor: 'center'
      };

      tb.loadObj(options, function(model) {
        var soldier = model.setCoords([-122.3491, 47.6207, 0]);
        soldier.addTooltip("This is a custom tooltip", true);
        tb.add(soldier);
      });
    },

    render: function(gl, matrix) {
      tb.update();
    }
  });
});

Fill-Extrusion Selection

Select and interact with Mapbox fill-extrusion building layers:
var minZoom = 12;

// Create building layer
function createBuildingLayer() {
  return {
    'id': '3d-buildings',
    'source': 'composite',
    'source-layer': 'building',
    'filter': ['==', 'extrude', 'true'],
    'type': 'fill-extrusion',
    'minzoom': minZoom,
    'paint': {
      'fill-extrusion-color': [
        'case',
        ['boolean', ['feature-state', 'select'], false],
        "lightgreen",  // Selected color
        ['boolean', ['feature-state', 'hover'], false],
        "lightblue",   // Hover color
        '#aaa'          // Default color
      ],
      'fill-extrusion-height': [
        'interpolate',
        ['linear'],
        ['zoom'],
        minZoom,
        0,
        minZoom + 0.05,
        ['get', 'height']
      ],
      'fill-extrusion-base': [
        'interpolate',
        ['linear'],
        ['zoom'],
        minZoom,
        0,
        minZoom + 0.05,
        ['get', 'min_height']
      ],
      'fill-extrusion-opacity': 0.9
    }
  };
}

map.on('style.load', function() {
  // Initialize Threebox with feature selection
  window.tb = new Threebox(
    map,
    map.getCanvas().getContext('webgl'),
    {
      defaultLights: true,
      enableSelectingFeatures: true,  // Enable building selection
      enableTooltips: true
    }
  );

  // Add building layer
  map.addLayer(createBuildingLayer());

  // Listen for feature selection events
  map.on('SelectedFeatureChange', onSelectedFeatureChange);

  // Update Threebox on render
  map.on('render', function() {
    tb.update();
  });
});

function onSelectedFeatureChange(e) {
  var feature = e.detail;
  
  if (feature && feature.state && feature.state.select) {
    console.log('Building selected:', feature.id);
    
    // Get feature center
    var coords = tb.getFeatureCenter(feature, null, 0);
    
    // Show popup
    new mapboxgl.Popup({ offset: 0 })
      .setLngLat([coords[0], coords[1]])
      .setHTML('<strong>' + (feature.id || feature.type) + '</strong>')
      .addTo(map);
    
    // Log GeoJSON
    var geoJson = {
      "geometry": feature.geometry,
      "type": "Feature",
      "properties": feature.properties
    };
    console.log(JSON.stringify(geoJson, null, 2));
  }
}

Mouse Events

Handle mouse interactions on objects:
var sphere = tb.sphere({
  radius: 30,
  color: 'red',
  material: 'MeshToonMaterial'
}).setCoords(origin);

// Mouse over
sphere.addEventListener('ObjectMouseOver', function(e) {
  console.log('Mouse over:', e.detail.name);
  sphere.setColor('yellow');
}, false);

// Mouse out
sphere.addEventListener('ObjectMouseOut', function(e) {
  console.log('Mouse out:', e.detail.name);
  sphere.setColor('red');
}, false);

// Selection change
sphere.addEventListener('SelectedChange', function(e) {
  if (e.detail.selected) {
    console.log('Selected');
  } else {
    console.log('Unselected');
  }
}, false);

tb.add(sphere);

All Interaction Events

tb.loadObj(options, function(model) {
  model.setCoords(origin);
  
  // Selection
  model.addEventListener('SelectedChange', function(e) {
    console.log('Selected:', e.detail.selected);
  }, false);
  
  // Mouse events
  model.addEventListener('ObjectMouseOver', function(e) {
    console.log('Mouse over:', e.detail.name);
  }, false);
  
  model.addEventListener('ObjectMouseOut', function(e) {
    console.log('Mouse out:', e.detail.name);
  }, false);
  
  // Dragging
  model.addEventListener('ObjectDragged', function(e) {
    console.log('Dragged action:', e.detail.draggedAction);
    console.log('Object:', e.detail.draggedObject);
  }, false);
  
  // General changes
  model.addEventListener('ObjectChanged', function(e) {
    console.log('Action:', e.detail.action);
    console.log('Object:', e.detail.object);
  }, false);
  
  // Wireframe toggle
  model.addEventListener('Wireframed', function(e) {
    console.log('Wireframe:', e.detail.wireframe);
  }, false);
  
  // Animation state
  model.addEventListener('IsPlayingChanged', function(e) {
    console.log('Is playing:', e.detail.isPlaying);
  }, false);
  
  tb.add(model);
});

Interactive Controls

Create UI buttons to control selected objects:
<div class="controls">
  <button id="wireButton" disabled>Wireframe</button>
  <button id="playButton" disabled>Play</button>
  <button id="deleteButton" disabled>Delete</button>
</div>

<script>
var selectedObject = null;

// Track selection
model.addEventListener('SelectedChange', function(e) {
  if (e.detail.selected) {
    selectedObject = e.detail;
    // Enable buttons
    document.getElementById('wireButton').disabled = false;
    document.getElementById('playButton').disabled = !e.detail.hasDefaultAnimation;
    document.getElementById('deleteButton').disabled = false;
  } else {
    selectedObject = null;
    // Disable buttons
    document.getElementById('wireButton').disabled = true;
    document.getElementById('playButton').disabled = true;
    document.getElementById('deleteButton').disabled = true;
  }
}, false);

// Wireframe toggle
document.getElementById('wireButton').addEventListener('click', function() {
  if (selectedObject) {
    selectedObject.wireframe = !selectedObject.wireframe;
  }
});

// Play/pause animation
document.getElementById('playButton').addEventListener('click', function() {
  if (selectedObject) {
    if (selectedObject.isPlaying) {
      selectedObject.stop();
    } else {
      selectedObject.playAnimation({ animation: 1, duration: 10000 });
    }
  }
});

// Delete object
document.getElementById('deleteButton').addEventListener('click', function() {
  if (selectedObject) {
    tb.remove(selectedObject);
    selectedObject = null;
  }
});
</script>

Tooltips

Default Tooltips

window.tb = new Threebox(
  map,
  mbxContext,
  {
    enableTooltips: true  // Enable default tooltips
  }
);

var sphere = tb.sphere({
  radius: 30,
  color: 'green'
}).setCoords(origin);

// Add tooltip
sphere.addTooltip("This is a sphere", true);
tb.add(sphere);

Custom Tooltips

function createCustomTooltip(text) {
  var div = document.createElement('div');
  div.className = 'custom-tooltip';
  div.innerHTML = `
    <h3>${text}</h3>
    <p>Click to select</p>
  `;
  return div;
}

model.addLabel(createCustomTooltip("Custom Label"), true);

Help Tooltips

window.tb = new Threebox(
  map,
  mbxContext,
  {
    enableTooltips: true,
    enableHelpTooltips: true  // Show interaction hints
  }
);

// Automatically shows help when:
// - Dragging: "Shift + Drag to move"
// - Altitude: "Ctrl + Drag for altitude"
// - Rotation: "Alt + Drag to rotate"

FOV and Camera Controls

Adjust field of view dynamically:
import { GUI } from 'https://threejs.org/examples/jsm/libs/lil-gui.module.min.js';

var api = {
  fov: Math.atan(3 / 4) * 180 / Math.PI,
  orthographic: false
};

var gui = new GUI();

// FOV slider (2.5 - 45 degrees)
gui.add(api, 'fov', 2.5, 45.0).step(0.1).onChange(function() {
  tb.fov = api.fov;
});

// Orthographic camera toggle
gui.add(api, 'orthographic').name('Orthographic').onChange(function() {
  tb.orthographic = api.orthographic;
  tb.fov = api.fov;
});
FOV values below 2.5 degrees or above 45 degrees can cause rendering issues.

Complete Interactive Example

Here’s a full example with all interaction features:
<!doctype html>
<head>
  <title>Threebox Interactions</title>
  <link href="https://api.mapbox.com/mapbox-gl-js/v2.2.0/mapbox-gl.css" rel="stylesheet">
  <script src="https://api.mapbox.com/mapbox-gl-js/v2.2.0/mapbox-gl.js"></script>
  <script src="../dist/threebox.js"></script>
  <link href="../dist/threebox.css" rel="stylesheet" />
  <style>
    body, html { width: 100%; height: 100%; margin: 0; }
    #map { width: 100%; height: 100%; }
    .help {
      position: absolute;
      top: 10px;
      left: 10px;
      background: rgba(0,0,0,0.7);
      color: white;
      padding: 10px;
      border-radius: 5px;
      font-size: 12px;
    }
  </style>
</head>
<body>
  <div id='map'></div>
  <div class="help">
    <strong>Interactions:</strong><br>
    Click to select<br>
    Shift + Drag to move<br>
    Ctrl + Drag for altitude<br>
    Alt + Drag to rotate
  </div>

  <script type="module">
    mapboxgl.accessToken = 'YOUR_TOKEN';

    var map = new mapboxgl.Map({
      container: 'map',
      style: 'mapbox://styles/mapbox/outdoors-v11',
      center: [-122.3491, 47.6207],
      zoom: 16.5,
      pitch: 60,
      antialias: true
    });

    map.on('style.load', function() {
      window.tb = new Threebox(
        map,
        map.getCanvas().getContext('webgl'),
        {
          defaultLights: true,
          enableSelectingObjects: true,
          enableDraggingObjects: true,
          enableRotatingObjects: true,
          enableTooltips: true,
          enableHelpTooltips: true
        }
      );

      map.addLayer({
        id: 'custom_layer',
        type: 'custom',
        renderingMode: '3d',
        onAdd: function(map, mbxContext) {
          tb.altitudeStep = 1;

          // Cube
          var geometry = new THREE.BoxGeometry(30, 60, 120);
          var cube = tb.Object3D({
            obj: new THREE.Mesh(geometry, new THREE.MeshPhongMaterial({
              color: 0x660000,
              side: THREE.DoubleSide
            })),
            units: 'meters'
          }).setCoords([-122.3512, 47.6202, 0]);
          cube.addTooltip("Red cube", true);
          tb.add(cube);

          // Sphere
          var sphere = tb.sphere({
            radius: 30,
            units: 'meters',
            color: 'green',
            material: 'MeshPhysicalMaterial'
          }).setCoords([-122.34548, 47.617538, 0]);
          sphere.addTooltip("Green sphere", true);
          tb.add(sphere);

          // Soldier
          var options = {
            obj: './models/soldier.glb',
            type: 'gltf',
            scale: 100,
            units: 'meters',
            rotation: { x: 90, y: 0, z: 0 },
            anchor: 'center'
          };

          tb.loadObj(options, function(model) {
            var soldier = model.setCoords([-122.3491, 47.6207, 0]);
            soldier.addTooltip("Soldier", true);
            
            soldier.addEventListener('SelectedChange', function(e) {
              console.log('Selected:', e.detail.selected);
            }, false);
            
            soldier.addEventListener('ObjectDragged', function(e) {
              console.log('Dragged:', e.detail.draggedAction);
            }, false);
            
            tb.add(soldier);
            soldier.playAnimation({ animation: 1, duration: 10000 });
          });
        },

        render: function(gl, matrix) {
          tb.update();
        }
      });
    });
  </script>
</body>

Next Steps

Examples Overview

Browse all interactive examples

Animations

Combine interactions with animations

3D Models

Load interactive 3D models

API Reference

Complete event API documentation

Build docs developers (and LLMs) love