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

> Learn to create a custom map control in GeoLibre by implementing the GeoLibrePlugin interface. This guide shows how to activate and deactivate custom controls for your mapping application.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: how-to-guide
- Published: 2026-08-01

---

**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`](https://github.com/opengeos/GeoLibre/blob/main/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.

```typescript
// 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.

```typescript
// 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:

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

```

The **plugin manager** at [`packages/plugins/src/plugin-manager.ts`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/packages/plugins/src/plugins/maplibre-basemap-control.ts) (lines 41-56) demonstrates how to restore previously saved layers when re-instantiating a control:

```typescript
// 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:

```typescript
// 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:

```typescript
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`](https://github.com/opengeos/GeoLibre/blob/main/maplibre-basemap-control.ts) |
| **Time slider** | Range input with throttled map updates | [`maplibre-time-slider.ts`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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`](https://github.com/opengeos/GeoLibre/blob/main/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.