---
title: "Track and update visited annotation states | Geosplats sdk"
description: "Mark annotations as visited and update pin icons at runtime."
---

> 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.

# Track and update visited annotation states

This example demonstrates how to use the `SplatAnnotation` runtime methods `setColor()`, `setIcon()`, and event listeners to track user engagement and mark houses as visited across a 6-house residential development.

### 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: [-74.42418, 40.77947],
  zoom: 18.2,
  pitch: 52,
  bearing: -30,
});

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

const VISITED_COLOR = "#e69439";
const visitedSet = new Set();
const annotationsList = [];

const houseConfigs = [
  {
    id: "house-1",
    pos: { x: 0.05, z: -0.58 },
    icon: "🏡",
    label: "House 1",
    popup:
      "<h3>🏡 House 1</h3><p>Two-story colonial with private driveway and landscaped lawn.</p>",
  },
  {
    id: "house-2",
    pos: { x: 0.35, z: -0.35 },
    icon: "🏡",
    label: "House 2",
    popup: "<h3>🏡 House 2</h3><p>Modern family residence featuring an open-concept patio.</p>",
  },
  {
    id: "house-3",
    pos: { x: -0.4, z: 0.05 },
    icon: "🏡",
    label: "House 3",
    popup: "<h3>🏡 House 3</h3><p>Spacious 4-bedroom home with attached two-car garage.</p>",
  },
  {
    id: "house-4",
    pos: { x: 0.4, z: 0.05 },
    icon: "🏡",
    label: "House 4",
    popup: "<h3>🏡 House 4</h3><p>Contemporary suburban home with south-facing solar roof.</p>",
  },
  {
    id: "house-5",
    pos: { x: -0.3, z: 0.42 },
    icon: "🏡",
    label: "House 5",
    popup:
      "<h3>🏡 House 5</h3><p>Corner lot cottage with mature oak trees and fenced backyard.</p>",
  },
  {
    id: "house-6",
    pos: { x: -0.55, z: -0.35 },
    icon: "🏡",
    label: "House 6",
    popup:
      "<h3>🏡 House 6</h3><p>Executive villa with walk-out basement and scenic garden view.</p>",
  },
];

function updateCounter() {
  const counter = document.getElementById("visitedCount");
  if (counter) {
    counter.textContent = `${visitedSet.size} / ${houseConfigs.length}`;
  }
}

map.on("splatmodel:load", () => {
  for (const config of houseConfigs) {
    const annotation = splatModel.addAnnotation({
      pos: config.pos,
      icon: config.icon,
      label: config.label,
      popup: config.popup,
      data: { id: config.id, defaultIcon: config.icon },
    });
    if (annotation) {
      annotationsList.push(annotation);
    }
  }
});

// 1. Mark with visited color when clicked
map.on("splatmodel:annotation:click", ({ annotation }) => {
  annotation.setColor(VISITED_COLOR);
  const data = annotation.getData();
  if (data && data.id) {
    visitedSet.add(data.id);
    updateCounter();
  }
});

// 2. Change icon to checkmark once popup closes
map.on("splatmodel:annotation:inactive", ({ annotation }) => {
  const data = annotation.getData();
  if (data && visitedSet.has(data.id)) {
    annotation.setIcon("✔️");
  }
});

// 3. Reset all visited annotations
document.getElementById("resetBtn")?.addEventListener("click", () => {
  visitedSet.clear();
  for (const ann of annotationsList) {
    const data = ann.getData();
    ann.setColor(undefined);
    if (data && data.defaultIcon) {
      ann.setIcon(data.defaultIcon);
    }
  }
  updateCounter();
});
```

#### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="robots" content="noindex" />
    <title>Track and update visited annotation states</title>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <div class="visited-panel">
      <div class="title">Visited Houses: <span id="visitedCount">0 / 6</span></div>
      <button id="resetBtn">Reset Visits</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;
}

.visited-panel {
  position: absolute;
  top: 16px;
  left: 16px;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(4px);
  padding: 12px 16px;
  border-radius: 8px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15);
  z-index: 10;
  font-size: 13px;
}

.visited-panel .title {
  font-weight: 600;
  color: #333;
  margin-bottom: 6px;
}

.visited-panel button {
  background: #f8f9fa;
  border: 1px solid #ced4da;
  padding: 4px 10px;
  border-radius: 4px;
  cursor: pointer;
  width: 100%;
  font-size: 12px;
  font-weight: 500;
}

.visited-panel button:hover {
  background: #e9ecef;
}
```

### 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>Track and update visited annotation states</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;
    }

    .visited-panel {
      position: absolute;
      top: 16px;
      left: 16px;
      background: rgba(255, 255, 255, 0.95);
      backdrop-filter: blur(4px);
      padding: 12px 16px;
      border-radius: 8px;
      box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15);
      z-index: 10;
      font-size: 13px;
    }

    .visited-panel .title {
      font-weight: 600;
      color: #333;
      margin-bottom: 6px;
    }

    .visited-panel button {
      background: #f8f9fa;
      border: 1px solid #ced4da;
      padding: 4px 10px;
      border-radius: 4px;
      cursor: pointer;
      width: 100%;
      font-size: 12px;
      font-weight: 500;
    }

    .visited-panel button:hover {
      background: #e9ecef;
    }
  </style>
</head>

<body>
  <div class="visited-panel">
    <div class="title">Visited Houses: <span id="visitedCount">0 / 6</span></div>
    <button id="resetBtn">Reset Visits</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: [-74.42418, 40.77947],
      zoom: 18.2,
      pitch: 52,
      bearing: -30,
    });

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

    const VISITED_COLOR = "#e69439";
    const visitedSet = new Set();
    const annotationsList = [];

    const houseConfigs = [
      {
        id: "house-1",
        pos: { x: 0.05, z: -0.58 },
        icon: "🏡",
        label: "House 1",
        popup:
          "<h3>🏡 House 1</h3><p>Two-story colonial with private driveway and landscaped lawn.</p>",
      },
      {
        id: "house-2",
        pos: { x: 0.35, z: -0.35 },
        icon: "🏡",
        label: "House 2",
        popup:
          "<h3>🏡 House 2</h3><p>Modern family residence featuring an open-concept patio.</p>",
      },
      {
        id: "house-3",
        pos: { x: -0.4, z: 0.05 },
        icon: "🏡",
        label: "House 3",
        popup: "<h3>🏡 House 3</h3><p>Spacious 4-bedroom home with attached two-car garage.</p>",
      },
      {
        id: "house-4",
        pos: { x: 0.4, z: 0.05 },
        icon: "🏡",
        label: "House 4",
        popup:
          "<h3>🏡 House 4</h3><p>Contemporary suburban home with south-facing solar roof.</p>",
      },
      {
        id: "house-5",
        pos: { x: -0.3, z: 0.42 },
        icon: "🏡",
        label: "House 5",
        popup:
          "<h3>🏡 House 5</h3><p>Corner lot cottage with mature oak trees and fenced backyard.</p>",
      },
      {
        id: "house-6",
        pos: { x: -0.55, z: -0.35 },
        icon: "🏡",
        label: "House 6",
        popup:
          "<h3>🏡 House 6</h3><p>Executive villa with walk-out basement and scenic garden view.</p>",
      },
    ];

    function updateCounter() {
      document.getElementById("visitedCount").textContent =
        `${visitedSet.size} / ${houseConfigs.length}`;
    }

    map.on("splatmodel:load", () => {
      for (const config of houseConfigs) {
        const annotation = splatModel.addAnnotation({
          pos: config.pos,
          icon: config.icon,
          label: config.label,
          popup: config.popup,
          data: { id: config.id, defaultIcon: config.icon },
        });
        if (annotation) {
          annotationsList.push(annotation);
        }
      }
    });

    // 1. Mark with visited color when clicked
    map.on("splatmodel:annotation:click", ({ annotation }) => {
      annotation.setColor(VISITED_COLOR);
      const data = annotation.getData();
      if (data && data.id) {
        visitedSet.add(data.id);
        updateCounter();
      }
    });

    // 2. Change icon to checkmark once popup closes
    map.on("splatmodel:annotation:inactive", ({ annotation }) => {
      const data = annotation.getData();
      if (data && visitedSet.has(data.id)) {
        annotation.setIcon("✔️");
      }
    });

    // 3. Reset all visited annotations
    document.getElementById("resetBtn").addEventListener("click", () => {
      visitedSet.clear();
      for (const ann of annotationsList) {
        const data = ann.getData();
        ann.setColor(undefined);
        if (data && data.defaultIcon) {
          ann.setIcon(data.defaultIcon);
        }
      }
      updateCounter();
    });
  </script>
</body>

</html>
```

## Listening to annotation events

The GeoSplats SDK provides three key annotation lifecycle events:

-   `"annotation:click"`: Fires when the user clicks a pin, right before its popup begins to open.
-   `"annotation:active"`: Fires when the popup opens on screen.
-   `"annotation:inactive"`: Fires when the popup closes (via the close button, clicking the map, or opening another popup).

Every event delivers the affected `annotation` handle in its payload.

## Updating pin colors dynamically

When a user clicks a house annotation, call `annotation.setColor()` to change its background fill immediately to an in-progress color:

```js
const VISITED_COLOR = "#e69439";

map.on("splatmodel:annotation:click", ({ annotation }) => {
  annotation.setColor(VISITED_COLOR);
});
```

The pin animates smoothly and maintains its new color even when the camera moves.

## Changing icons when popups close

When the user finishes reading a popup and closes it, the `"annotation:inactive"` event fires. Use `annotation.setIcon()` to replace the pin’s house icon with a completion badge like a checkmark:

```js
map.on("splatmodel:annotation:inactive", ({ annotation }) => {
  annotation.setIcon("✔️");
});
```

The pin automatically recalculates its geometry and resizes with a brief squash-and-stretch animation.

## Resetting annotation properties

Calling `annotation.setColor(undefined)` removes the custom color and restores the default MapTiler blue theme:

```js
// Reset pin to default theme
annotation.setColor(undefined);

// Restore original icon from custom data
const data = annotation.getData();
if (data?.defaultIcon) {
  annotation.setIcon(data.defaultIcon);
}
```

## Learn more

Check out the [MapTiler GeoSplats SDK reference](https://docs.maptiler.com/geosplats-sdk/api) to learn more about the `SplatAnnotation` API and event parameters.

## 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.

### [Guided tour with annotations](https://docs.maptiler.com/geosplats-sdk/examples/annotation-guided-tour/)

Examples

Guide users through model annotations with next and previous buttons.

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

Examples

Filter annotations by category using visibility toggle or color dimming.
