---
title: "Create a guided tour with annotations | Geosplats sdk"
description: "Guide users through model annotations with next and previous buttons."
---

> For AI agents: a documentation index is available at [/llms.txt](https://docs.maptiler.com/llms.txt). Markdown versions of all pages are available by appending .md to any URL.

# Create a guided tour with annotations

This example demonstrates how to build a guided tour by controlling annotations programmatically with `openPopup()` and `closePopup()`.

### NPM module setup

```bash
npm install --save @maptiler/geosplats
```

#### main.js

```js
import { SplatModel, Map, config } from "@maptiler/geosplats";
import "@maptiler/geosplats/dist/maptiler-geosplats.css";

config.apiKey = "YOUR_MAPTILER_API_KEY_HERE";

const map = new Map({
  apiKey: config.apiKey,
  container: "map",
  center: [-80.27822, 25.74077],
  zoom: 17.5,
  pitch: 50,
  bearing: -15,
});

let splatModel;
map.on("load", () => {
  splatModel = new SplatModel({
    model: "YOUR_MAPTILER_SPLAT_MODEL_ID_HERE",
    revealParams: { enabled: true },
  });
  map.addSplatModel(splatModel);
});

const tour = [];
let currentStep = 0;

function goTo(index) {
  if (!tour.length) return;
  currentStep = (index + tour.length) % tour.length;
  const indicator = document.getElementById("stepIndicator");
  if (indicator) {
    indicator.textContent = `Step ${currentStep + 1} of ${tour.length}`;
  }
  tour[currentStep].openPopup();
}

map.on("splatmodel:load", () => {
  const a1 = splatModel.addAnnotation({
    pos: { x: -0.06, z: -0.26 },
    icon: "🏢",
    label: "1. Main Lobby",
    popup: "<h3>1. Main Lobby</h3><p>Grand entrance hall and 24/7 concierge reception.</p>",
  });

  const a2 = splatModel.addAnnotation({
    pos: { x: -0.4, z: -0.2 },
    icon: "🏖️",
    label: "2. Sun Pool",
    popup: "<h3>2. Sun Pool</h3><p>Heated resort pool surrounded by palm groves and cabanas.</p>",
  });

  const a3 = splatModel.addAnnotation({
    pos: { x: 0.6, z: 0.46 },
    icon: "🎾",
    label: "3. Tennis Club",
    popup: "<h3>3. Tennis Club</h3><p>10 floodlit championship clay courts.</p>",
  });

  const a4 = splatModel.addAnnotation({
    pos: { x: -0.6, z: 0.5 },
    icon: "⛳",
    label: "4. Golf Course",
    popup:
      "<h3>4. Golf Course</h3><p>18-hole championship green surrounding the resort grounds.</p>",
  });

  tour.push(a1, a2, a3, a4);
  goTo(0);
});

document.getElementById("nextBtn")?.addEventListener("click", () => goTo(currentStep + 1));
document.getElementById("prevBtn")?.addEventListener("click", () => goTo(currentStep - 1));

document.addEventListener("keydown", (e) => {
  if (e.key === "ArrowRight") {
    goTo(currentStep + 1);
  } else if (e.key === "ArrowLeft") {
    goTo(currentStep - 1);
  } else if (e.key === "Escape") {
    tour[currentStep]?.closePopup();
  }
});
```

#### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="robots" content="noindex" />
    <title>Create a guided tour with annotations</title>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <div class="tour-control">
      <button id="prevBtn">◀ Prev</button>
      <span id="stepIndicator">Step 1 of 4</span>
      <button id="nextBtn">Next ▶</button>
    </div>
    <div id="map"></div>
    <script type="module" src="./main.js"></script>
  </body>
</html>
```

#### style.css

```css
body {
  font-family: Inter, sans-serif;
  margin: 0;
  overflow: hidden;
}

#map {
  width: 100%;
  height: 100vh;
}

.tour-control {
  position: absolute;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(4px);
  padding: 8px 16px;
  border-radius: 30px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  display: flex;
  align-items: center;
  gap: 12px;
  z-index: 10;
}

.tour-control button {
  background: #3174ff;
  color: #fff;
  border: none;
  padding: 6px 14px;
  border-radius: 16px;
  font-weight: 600;
  font-size: 13px;
  cursor: pointer;
}

.tour-control button:hover {
  background: #1b5ce5;
}

.tour-control span {
  font-size: 14px;
  font-weight: 600;
  color: #333;
  min-width: 90px;
  text-align: center;
}
```

### Basic JavaScript setup

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Create a guided tour with annotations</title>
    <link
      rel="stylesheet"
      href="https://cdn.maptiler.com/maptiler-geosplats/v1.2.1/maptiler-geosplats.css"
    />
    <style>
      body {
        font-family: Inter, sans-serif;
        margin: 0;
        overflow: hidden;
      }

      #map {
        width: 100%;
        height: 100vh;
      }

      .tour-control {
        position: absolute;
        top: 16px;
        left: 50%;
        transform: translateX(-50%);
        background: rgba(255, 255, 255, 0.95);
        backdrop-filter: blur(4px);
        padding: 8px 16px;
        border-radius: 30px;
        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
        display: flex;
        align-items: center;
        gap: 12px;
        z-index: 10;
      }

      .tour-control button {
        background: #3174ff;
        color: #fff;
        border: none;
        padding: 6px 14px;
        border-radius: 16px;
        font-weight: 600;
        font-size: 13px;
        cursor: pointer;
      }

      .tour-control button:hover {
        background: #1b5ce5;
      }

      .tour-control span {
        font-size: 14px;
        font-weight: 600;
        color: #333;
        min-width: 90px;
        text-align: center;
      }
    </style>
  </head>

  <body>
    <div class="tour-control">
      <button id="prevBtn">◀ Prev</button>
      <span id="stepIndicator">Step 1 of 4</span>
      <button id="nextBtn">Next ▶</button>
    </div>
    <div id="map"></div>
    <script type="module">
      import {
        SplatModel,
        Map,
        config,
      } from "https://cdn.maptiler.com/maptiler-geosplats/v1.2.1/maptiler-geosplats.mjs";

      config.apiKey = "YOUR_MAPTILER_API_KEY_HERE";

      const map = new Map({
        apiKey: config.apiKey,
        container: "map",
        center: [-80.27822, 25.74077],
        zoom: 17.5,
        pitch: 50,
        bearing: -15,
      });

      let splatModel;
      map.on("load", () => {
        splatModel = new SplatModel({
          model: "YOUR_MAPTILER_SPLAT_MODEL_ID_HERE",
          revealParams: { enabled: true },
        });
        map.addSplatModel(splatModel);
      });

      const tour = [];
      let currentStep = 0;

      function goTo(index) {
        if (!tour.length) return;
        currentStep = (index + tour.length) % tour.length;
        document.getElementById("stepIndicator").textContent =
          `Step ${currentStep + 1} of ${tour.length}`;
        tour[currentStep].openPopup();
      }

      map.on("splatmodel:load", () => {
        const a1 = splatModel.addAnnotation({
          pos: { x: -0.06, z: -0.26 },
          icon: "🏢",
          label: "1. Main Lobby",
          popup: "<h3>1. Main Lobby</h3><p>Grand entrance hall and 24/7 concierge reception.</p>",
        });

        const a2 = splatModel.addAnnotation({
          pos: { x: -0.4, z: -0.2 },
          icon: "🏖️",
          label: "2. Sun Pool",
          popup:
            "<h3>2. Sun Pool</h3><p>Heated resort pool surrounded by palm groves and cabanas.</p>",
        });

        const a3 = splatModel.addAnnotation({
          pos: { x: 0.6, z: 0.46 },
          icon: "🎾",
          label: "3. Tennis Club",
          popup: "<h3>3. Tennis Club</h3><p>10 floodlit championship clay courts.</p>",
        });

        const a4 = splatModel.addAnnotation({
          pos: { x: -0.6, z: 0.5 },
          icon: "⛳",
          label: "4. Golf Course",
          popup:
            "<h3>4. Golf Course</h3><p>18-hole championship green surrounding the resort grounds.</p>",
        });

        tour.push(a1, a2, a3, a4);
        goTo(0);
      });

      document.getElementById("nextBtn").addEventListener("click", () => goTo(currentStep + 1));
      document.getElementById("prevBtn").addEventListener("click", () => goTo(currentStep - 1));

      document.addEventListener("keydown", (e) => {
        if (e.key === "ArrowRight") {
          goTo(currentStep + 1);
        } else if (e.key === "ArrowLeft") {
          goTo(currentStep - 1);
        } else if (e.key === "Escape") {
          tour[currentStep]?.closePopup();
        }
      });
    </script>
  </body>
</html>
```

## Opening popups programmatically

While popups typically open on user clicks, you can trigger them directly in JavaScript using the `SplatAnnotation` instance handle.

Calling `annotation.openPopup()` opens the annotation’s popup and automatically closes whichever popup was previously open:

```js
const tour = []; // SplatAnnotation handles

function goTo(stepIndex) {
  tour[stepIndex].openPopup();
}
```

Unlike user clicks, `openPopup()` does not fire `"annotation:click"`, but it fires `"annotation:active"` on the model and `"splatmodel:annotation:active"` on the map.

## Navigating between tour steps

Store the returned `SplatAnnotation` handles in an array when adding annotations during `splatmodel:load`. You can then step forward or backward using external UI buttons:

```js
let currentStep = 0;

document.getElementById("nextBtn").addEventListener("click", () => {
  currentStep = (currentStep + 1) % tour.length;
  tour[currentStep].openPopup();
});
```

When a popup opens, the active pin bounces and a pulse ring ripples across the model surface, directing the viewer’s attention to the active point of interest.

## Keyboard shortcuts and escape handling

You can close any open popup programmatically with `closePopup()`. This makes it simple to bind keyboard shortcuts like `Escape` or the left and right arrow keys:

```js
document.addEventListener("keydown", (event) => {
  if (event.key === "Escape") {
    tour[currentStep]?.closePopup();
  }
});
```

## Learn more

Check out the [MapTiler GeoSplats SDK reference](https://docs.maptiler.com/geosplats-sdk/api) to explore the `SplatAnnotation` interface.

## Related examples

### [Add annotations](https://docs.maptiler.com/geosplats-sdk/examples/add-annotations/)

Examples

Add interactive labelled pins and popups to a splat model.

### [Pin styles and categories](https://docs.maptiler.com/geosplats-sdk/examples/annotation-styles/)

Examples

Style annotations with colors, icons, numbers, and custom popup placement.

### [Filter annotations by category](https://docs.maptiler.com/geosplats-sdk/examples/filter-annotations/)

Examples

Filter annotations by category using visibility toggle or color dimming.

### [Sync annotations with sidebar](https://docs.maptiler.com/geosplats-sdk/examples/sync-annotations-sidebar/)

Examples

Connect 3D model annotations bidirectionally with an external sidebar list.
