Skip to main content

Overview

Threadbox uses CSS2DRenderer to display HTML labels and tooltips that stay anchored to 3D objects. These labels are rendered as HTML/CSS, allowing for rich styling and interactivity while automatically handling occlusion and positioning.

Label Renderer

Threadbox automatically creates a LabelRenderer that overlays the map canvas:
const tb = new Threebox(map, gl, {
  defaultLights: true,
  enableTooltips: true // Enable default tooltips
});

// Label renderer is automatically available
console.log(tb.labelRenderer);

Labels

Creating Labels

Add persistent labels to objects:
// Create a label object
const label = tb.label({
  htmlElement: '<div>City Hall</div>',
  cssClass: 'custom-label',
  alwaysVisible: true,
  feature: geoJsonFeature
});

label.setCoords([-122.4194, 37.7749, 50]);
tb.add(label);

Label Options

OptionTypeDefaultDescription
htmlElementstring or HTMLElementRequiredHTML content for the label
cssClassstring'label3D'CSS class for styling
alwaysVisiblebooleanfalseWhether label is always visible
featureobjectnullGeoJSON feature data

Adding Labels to Models

Add labels to 3D models:
tb.loadObj({
  obj: '/models/building.glb',
  type: 'gltf',
  scale: 1,
  units: 'meters'
}, (model) => {
  model.setCoords([-122.4194, 37.7749]);
  tb.add(model);
  
  // Add label to model
  const labelHTML = '<div class="building-label">Office Building</div>';
  model.addLabel(
    labelHTML,
    true,           // visible
    model.anchor,   // center position
    0.5             // height multiplier
  );
});

Removing Labels

// Remove label from object
model.removeLabel();

// Access label before removing
if (model.label) {
  console.log('Label exists');
  model.removeLabel();
}

Tooltips

Default Tooltips

Enable automatic tooltips for all objects:
const tb = new Threebox(map, gl, {
  enableTooltips: true,
  enableSelectingObjects: true
});

// Tooltips will show automatically on hover/select

Creating Tooltips

Create standalone tooltip objects:
const tooltip = tb.tooltip({
  text: 'Click for more info',
  mapboxStyle: true, // Use Mapbox popup style
  feature: geoJsonFeature
});

tooltip.setCoords([-122.4194, 37.7749, 25]);
tb.add(tooltip);

// Initially hidden
tooltip.tooltip.visible = false;

Tooltip Options

OptionTypeDefaultDescription
textstringRequiredTooltip text content
mapboxStylebooleanfalseUse Mapbox GL popup styling
featureobjectnullGeoJSON feature data

Adding Tooltips to Models

model.addTooltip(
  'Interactive Model',  // tooltip text
  true,                 // use Mapbox style
  model.anchor,         // center position
  true,                 // custom tooltip
  1.0                   // height multiplier
);

// Show/hide tooltip
if (model.tooltip) {
  model.tooltip.visible = true;
}

// Remove tooltip
model.removeTooltip();

Dynamic Tooltip Content

model.addEventListener('ObjectMouseOver', (e) => {
  const object = e.detail;
  
  // Create dynamic tooltip
  const tooltipText = `
    <strong>${object.userData.name}</strong><br>
    Height: ${object.coordinates[2]}m<br>
    Type: ${object.userData.type}
  `;
  
  object.addTooltip(tooltipText, true);
  object.tooltip.visible = true;
});

model.addEventListener('ObjectMouseOut', (e) => {
  if (e.detail.tooltip) {
    e.detail.tooltip.visible = false;
  }
});

Help Tooltips

Show contextual help during interactions:
const tb = new Threebox(map, gl, {
  enableHelpTooltips: true // Enable help tooltips
});

// Add help tooltip to object
model.addHelp(
  'Press Shift+Drag to move',
  'help',          // object name
  false,           // not Mapbox style
  model.anchor,    // position
  0.5              // height
);

// Remove help tooltip
model.removeHelp();

Custom Styling

CSS Classes

Default CSS classes:
/* Default label style */
.label3D {
  background-color: rgba(255, 255, 255, 0.9);
  border-radius: 4px;
  padding: 4px 8px;
  font-size: 12px;
  font-weight: bold;
  pointer-events: none;
}

/* Default tooltip style */
.tooltip3D {
  background-color: rgba(0, 0, 0, 0.8);
  color: white;
  border-radius: 3px;
  padding: 5px 10px;
  font-size: 11px;
  white-space: nowrap;
  pointer-events: none;
}

Custom Label Styles

// Create label with custom class
const label = tb.label({
  htmlElement: '<div>Custom Styled Label</div>',
  cssClass: 'my-custom-label'
});
/* Custom label styling */
.my-custom-label {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 8px 16px;
  border-radius: 20px;
  font-weight: bold;
  box-shadow: 0 4px 6px rgba(0,0,0,0.3);
  pointer-events: auto; /* Make it clickable */
  cursor: pointer;
}

.my-custom-label:hover {
  transform: scale(1.05);
  box-shadow: 0 6px 12px rgba(0,0,0,0.4);
}

Mapbox Style Tooltips

Use Mapbox GL JS popup styling:
const tooltip = tb.tooltip({
  text: 'Building Information',
  mapboxStyle: true // Uses Mapbox popup classes
});
This generates:
<div class="label3D">
  <div class="marker mapboxgl-popup-anchor-bottom">
    <div class="mapboxgl-popup-tip"></div>
    <div class="mapboxgl-popup-content">
      <strong>Building Information</strong>
    </div>
  </div>
</div>

Label Positioning

Anchor Points

Position labels relative to object anchor:
model.addLabel(
  '<div>Label Text</div>',
  true,
  model.center,    // Use center anchor
  1.0              // At top of object
);

// Available anchors:
// model.center, model.top, model.bottom
// model.topLeft, model.topRight
// model.bottomLeft, model.bottomRight
// model.left, model.right

Height Adjustment

// Label at 50% of object height
model.addLabel(html, true, model.anchor, 0.5);

// Label at top of object
model.addLabel(html, true, model.anchor, 1.0);

// Label above object
model.addLabel(html, true, model.anchor, 1.5);

Direct CSS2D Objects

Create CSS2D objects directly:
const div = document.createElement('div');
div.className = 'custom-label';
div.textContent = 'Advanced Label';

const css2DLabel = model.addCSS2D(
  div,
  'customLabel',   // object name
  model.anchor,    // center
  1.0              // height
);

// Control visibility
css2DLabel.visible = true;

// Remove
model.removeCSS2D('customLabel');

Visibility Control

Always Visible Labels

const label = tb.label({
  htmlElement: '<div>Permanent Label</div>',
  alwaysVisible: true // Always shown
});

label.setCoords(coordinates);
tb.add(label);

Conditional Visibility

// Show label only when selected
model.addEventListener('SelectedChange', (e) => {
  if (model.label) {
    model.label.visible = e.detail.selected;
  }
});

// Show label on hover
model.addEventListener('ObjectMouseOver', () => {
  if (model.label) model.label.visible = true;
});

model.addEventListener('ObjectMouseOut', () => {
  if (model.label && !model.label.alwaysVisible) {
    model.label.visible = false;
  }
});

Layer Visibility

Toggle labels by layer:
// Hide all labels in a layer
tb.labelRenderer.toggleLabels('buildings-layer', false);

// Show all labels in a layer
tb.labelRenderer.toggleLabels('buildings-layer', true);

Feature Tooltips

Automatic tooltips for fill-extrusion features:
const tb = new Threebox(map, gl, {
  enableSelectingFeatures: true,
  enableTooltips: true
});

// Tooltips automatically appear on hover/select
// Content from feature.properties.name or feature.id

Complete Example

map.on('load', () => {
  const tb = new Threebox(map, gl, {
    defaultLights: true,
    enableSelectingObjects: true,
    enableTooltips: true,
    enableHelpTooltips: true
  });
  
  map.addLayer({
    id: 'custom-layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function(map, gl) {
      // Create permanent label
      const cityLabel = tb.label({
        htmlElement: '<div class="city-label">San Francisco</div>',
        cssClass: 'city-label',
        alwaysVisible: true
      });
      cityLabel.setCoords([-122.4194, 37.7749, 100]);
      tb.add(cityLabel);
      
      // Load model with dynamic tooltip
      tb.loadObj({
        obj: '/models/building.glb',
        type: 'gltf',
        scale: 1,
        units: 'meters'
      }, (building) => {
        building.setCoords([-122.4194, 37.7749, 0]);
        tb.add(building);
        
        // Add label
        building.addLabel(
          '<div class="building-name">Office Building</div>',
          false,          // not always visible
          building.center,
          1.0
        );
        
        // Show label and tooltip on hover
        building.addEventListener('ObjectMouseOver', () => {
          if (building.label) {
            building.label.visible = true;
          }
          
          // Add dynamic tooltip
          const info = `
            <strong>Office Building</strong><br>
            Height: ${building.modelHeight.toFixed(1)}m<br>
            <em>Click for details</em>
          `;
          building.addTooltip(info, true);
          building.tooltip.visible = true;
        });
        
        building.addEventListener('ObjectMouseOut', () => {
          if (building.label && !building.selected) {
            building.label.visible = false;
          }
          if (building.tooltip && !building.selected) {
            building.tooltip.visible = false;
          }
        });
        
        // Keep label visible when selected
        building.addEventListener('SelectedChange', (e) => {
          if (building.label) {
            building.label.visible = e.detail.selected;
          }
        });
      });
    },
    render: function(gl, matrix) {
      tb.update();
    }
  });
});

Performance Tips

Optimize Label Performance

  • Limit the number of always-visible labels
  • Use alwaysVisible: false and show on hover/select
  • Disable tooltips globally if not needed: enableTooltips: false
  • Remove labels from off-screen objects
  • Use simple HTML/CSS (avoid complex animations)

See Also

Build docs developers (and LLMs) love