How to Create a Custom Map Control in GeoLibre: The Complete Plugin Guide

Create a custom map control in GeoLibre by implementing the GeoLibrePlugin interface, instantiating any MapLibre-GL IControl object in the activate callback, and attaching it with app.addMapControl(control, position), then clean up in deactivate with app.removeMapControl(control).

Creating custom map controls in GeoLibre leverages a plugin-based architecture where controls are first-class plugins that hook into the core application. This approach ensures your custom controls integrate cleanly with GeoLibre's state management, respect runtime environment changes, and follow predictable lifecycle patterns. Whether you're building a simple toolbar button or a complex interactive panel, the same fundamental pattern applies.

Understanding the GeoLibre Plugin Architecture

GeoLibre's control system is built on a minimal but powerful abstraction. The core exposes the GeoLibreAppAPI interface, which provides methods for adding, removing, and repositioning map controls.

The GeoLibrePlugin Interface (from packages/plugins/src/types.ts)

Every control plugin must implement this contract:

  • id: Unique string identifier for the plugin
  • name: Human-readable display name
  • version: Semver string for compatibility tracking
  • activate(app): Called when the plugin loads; create and attach your control here
  • deactivate(app): Called when the plugin unloads; remove your control and clean up
  • getMapControlPosition (optional): Return the current position string
  • setMapControlPosition (optional): Handle runtime position changes from the UI

Control Position Options

GeoLibre uses MapLibre-GL's standard corner positions:

  • "top-left" — Above the zoom controls
  • "top-right" — Opposite side, common for custom toolbars
  • "bottom-left" — Below attribution
  • "bottom-right" — Common for legends and time sliders

Step-by-Step: Building Your First Custom Control

Follow this sequence to create a production-ready control plugin.

Step 1: Create the MapLibre Control Class

Your control must implement MapLibre's IControl interface with onAdd(map) and onRemove() methods.

// packages/plugins/src/plugins/alert-center-control.ts
import type { IControl, Map as MapLibreMap } from "maplibre-gl";

class AlertCenterControl implements IControl {
  private _container: HTMLElement | null = null;

  onAdd(map: MapLibreMap): HTMLElement {
    this._container = document.createElement("div");
    this._container.className = "alert-center-control maplibregl-ctrl-group";

    const button = document.createElement("button");
    button.title = "Show Map Center";
    button.innerText = "📍";
    button.onclick = () => {
      const center = map.getCenter();
      alert(`Center: ${center.lng.toFixed(4)}, ${center.lat.toFixed(4)}`);
    };

    this._container.appendChild(button);
    return this._container;
  }

  onRemove(): void {
    // Clean up any event listeners or external refs
    this._container?.remove();
    this._container = null;
  }
}

Step 2: Wrap It in a GeoLibre Plugin

The plugin connects your control to GeoLibre's lifecycle.

// packages/plugins/src/plugins/alert-center-control.ts (continued)
import type { GeoLibreAppAPI, GeoLibreMapControlPosition, GeoLibrePlugin } from "../types";

export const alertCenterControlPlugin: GeoLibrePlugin = {
  id: "alert-center-control",
  name: "Center Alert Button",
  version: "1.0.0",

  activate(app: GeoLibreAppAPI): void {
    const control = new AlertCenterControl();
    
    // Attempt to add immediately; handle deferred load if map not ready
    const added = app.addMapControl(control, "top-right");
    
    if (!added) {
      console.warn("[alert-center-control] Map not ready, control queued");
    }

    // Store reference for cleanup and position changes
    (app as any)._alertCenterControl = control;
  },

  deactivate(app: GeoLibreAppAPI): void {
    const control = (app as any)._alertCenterControl;
    if (control) {
      app.removeMapControl(control);
      delete (app as any)._alertCenterControl;
    }
  },

  // Optional: enable runtime repositioning via settings UI
  getMapControlPosition(): GeoLibreMapControlPosition {
    return "top-right";
  },

  setMapControlPosition(app: GeoLibreAppAPI, position: GeoLibreMapControlPosition): boolean {
    const control = (app as any)._alertCenterControl;
    if (!control) return false;
    
    app.removeMapControl(control);
    return app.addMapControl(control, position);
  }
};

Step 3: Register the Plugin

Export from the plugins barrel file so the plugin manager discovers it:

// packages/plugins/src/index.ts
export { alertCenterControlPlugin } from "./plugins/alert-center-control";

The plugin manager at packages/plugins/src/plugin-manager.ts handles the rest—loading, version checking, and calling activate/deactivate at appropriate times.

Production Patterns from the Source Code

Study these reference implementations in the GeoLibre repository for advanced patterns.

Pattern 1: State Persistence and Restoration

The Basemap Control in packages/plugins/src/plugins/maplibre-basemap-control.ts (lines 41-56) demonstrates how to restore previously saved layers when re-instantiating a control:

// Simplified excerpt showing state restoration pattern
function relinkRestoredRasterBasemaps(app: GeoLibreAppAPI, control: BasemapControl): void {
  const savedState = app.getPersistentState("basemap-layers");
  if (savedState) {
    control.restoreLayers(savedState);
  }
}

Pattern 2: Reacting to Environment Changes

Lines 21-32 of the same file show runtime environment handling via addRuntimeEnvListener, letting controls adapt without reload:

// From maplibre-basemap-control.ts
app.addRuntimeEnvListener((env) => {
  control.updateAvailableBasemaps(env.allowedBasemapProviders);
});

Pattern 3: Deferred Control Attachment

For controls that depend on async initialization, check the return value of addMapControl and retry on the "mapready" event:

activate(app: GeoLibreAppAPI): void {
  const tryAdd = () => {
    const control = new TimeSliderControl();
    const success = app.addMapControl(control, "bottom-right");
    if (success) {
      (app as any)._timeSlider = control;
    } else {
      // Retry once map becomes available
      app.once("mapready", tryAdd);
    }
  };
  tryAdd();
}

Common Control Types and Implementation Tips

Control Type Key Technique Reference File
Toolbar button Simple IControl with event listener Pattern shown above
Layer switcher Track active layers in plugin state, sync with app store maplibre-basemap-control.ts
Time slider Range input with throttled map updates maplibre-time-slider.ts
Draw/measure tool Integrate existing MapLibre plugins (e.g., maplibre-gl-draw) Wrap with plugin lifecycle
Fullscreen panel Render React/Vue component via portal, position off-map Use top-left with custom CSS

Summary

  • Custom map controls in GeoLibre are plugins that implement the GeoLibrePlugin interface defined in packages/plugins/src/types.ts.
  • Create standard MapLibre-GL controls by implementing the IControl interface with onAdd and onRemove methods.
  • Attach controls via app.addMapControl(control, position) in the activate callback; positions are "top-left", "top-right", "bottom-left", or "bottom-right".
  • Clean up with app.removeMapControl(control) in deactivate to prevent memory leaks and DOM residue.
  • Enable position changes by implementing getMapControlPosition and setMapControlPosition for UI-driven repositioning.
  • Study built-in plugins in packages/plugins/src/plugins/ for state management, environment reactivity, and deferred loading patterns.

Frequently Asked Questions

What happens if I call addMapControl before the map is ready?

The method returns false, and you should retry on the "mapready" event. Most built-in plugins follow this pattern to handle race conditions during startup.

Can I use existing MapLibre-GL plugins as GeoLibre controls?

Yes. Any object implementing IControl works—wrap it in a plugin following the lifecycle pattern above. Popular choices include maplibre-gl-draw, maplibre-gl-geocoder, and maplibre-gl-inspect.

How do I style my custom control?

Add CSS classes following MapLibre conventions: maplibregl-ctrl for the outer container, maplibregl-ctrl-group for button groups, and maplibregl-ctrl-icon for icon buttons. GeoLibre's theme variables are available for consistent styling.

Where does the control position appear in the UI?

The Controls Menu at apps/geolibre-desktop/src/components/layout/toolbar/ControlsMenu.tsx lists loaded controls and can trigger position changes if your plugin implements the optional position methods.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →