# Heatmaps

> Where visitors click, how far down they scroll and which parts of a page they see, per screen size, drawn over your own page from the dashboard.

A heatmap shows what visitors do on one page: where they click, how far down they get, and which parts of it they actually look at. odo’s script records it on every page with nothing to set up, the dashboard sums it up page by page, and odo can draw it over your own page.

## What’s recorded

| Layer | What the script notes on each page view |
| --- | --- |
| Clicks | Every click made by hand (not one from the keyboard or from the page’s own code): what it landed on (the link or button it was in, else the element itself), where in that element, and how far down the page. At most 40 a page view. |
| Rage and dead clicks | The same spot clicked again and again in a burst, and a click that changed nothing, marked on the element they were on. They’re part of the `clicks` group: see [clicks, forms and errors](https://odo.gg/docs/interactions). |
| Scroll | The deepest point reached, as a share of the page’s height (the bottom of the screen counts), and how many screens tall the page was. |
| Attention | Each part of the page that came on screen, and how long it was at least half in view (or filled half the screen), counted only while the tab was in front of them. Up to 20 parts a page. |

Scroll and attention go as one summary, sent when the page is hidden or left; a later one for the same page view replaces it.

### Phones, tablets and desktops

A page’s layout follows the width of the browser window, so every heatmap is read per screen: **Phones** (under 640 pixels wide), **Tablets** (640 to 1,023) and **Desktops** (1,024 and wider), or all of them together. Only the class is kept, never the width itself.

## Turn it on or off

Heatmaps are on for every site: they’re the `heat` group of what the script records. To leave them out, list the groups you want on the tag, without `heat`:

```html
<script defer src="https://odo.gg/js/odo.js" data-site="SITE_ID" data-capture="clicks forms errors sections vitals"></script>
```

`data-capture="heat"` keeps heatmaps alone (rage and dead clicks need `clicks` too), and `data-capture="none"` keeps none of it. To stop heatmaps along with everything else the script notes about what people do, turn off **Clicks, forms, errors and page speed** in **Settings → Websites**, under the site’s **Options**. It’s on unless you turn it off, and experiments are recorded with it too.

Heatmaps come from odo’s script as it is now. A browser still holding an older copy picks up the new one within a day; until a page has heatmaps, its scroll reach is read from how far each visit scrolled, every screen together.

## Name the parts of a page

Attention is read part by part. Name the parts that matter with `data-odo-section`:

```html
<section data-odo-section="Pricing">…</section>
<form data-odo-section="Signup form">…</form>
```

Without any, odo watches the page’s own main parts: its `header`, each child of `main`, every `section` with an `id`, and its `footer`. Each is named by its `data-odo-section`, `aria-label` or `id`, else by its heading when that’s short, else by what it is (`header`, `div`).

- Once anything on a page has `data-odo-section`, only the marked parts are watched there, so mark every part you care about.
- Only the outermost parts count: a part inside another is read as part of it.
- Names are cut to 40 characters. Never put personal data in one.
- Nothing inside `data-odo-ignore` is watched.

The same attribute says where on the page a click happened, and which sections were seen: see [clicks, forms and errors](https://odo.gg/docs/interactions#name-things-yourself).

## Open a page’s heatmap

On the Traffic dashboard or the Overview, tap a page in the **Pages** card (**Top**, **Entry** or **Exit**). Its sheet reads the range and filters on screen, for **All** screens or **Phones**, **Tablets** or **Desktops** alone:

- **Scroll reach:** the share of visitors who got each 5% of the way down, where half of them stop, how many reach the end, and how many screens tall the page is.
- **Clicks:** what they click, most first, with each one’s share of the page’s clicks and its rage and dead clicks. Links, buttons and fields are named by their own label; anything else by where it is.
- **Attention:** the parts of the page in its order, the share of visitors who saw each, and how long it stayed in view (the median).

**Filter the dashboard to this page** narrows everything else to it. Owners, admins and managers see a page’s heatmap; a shared view with its traffic shows the sheet too, without the button below.

### On your own page

**View heatmap on your site** opens the page itself, on your site, with the heatmap drawn over it:

- **Clicks:** heat where the clicks landed, the five most clicked elements outlined and ranked, and rage and dead clicks marked in red.
- **Scroll:** the page shaded by how many visitors saw each part of it, with lines where 75%, half and 25% of them had stopped.
- **Attention:** each part tinted by how many saw it, with how long it was in view.

A small panel switches the layer, the screen and the range (7, 30 or 90 days), folds away, and closes. Clicks are placed on each element by where it is and where in it they landed, so they follow the page as it’s laid out now; when 5% or more of them were on things the page doesn’t show now, the panel says so. For a screen other than your window’s, it says how wide to make the window to see the page as those visitors did.

### How the link works

The button opens your page with a token in the address’s fragment, the part after `#`: `https://your.site/pricing#odo-heatmap=…`. A fragment never leaves the browser, so your server, its logs and other sites’ referrers never see it. The token opens that one page’s heatmap, summed up, for an hour, and nothing else: no visit, visitor or text.

odo’s script sees it, measures nothing on that page (you looking isn’t a visit), and loads the overlay, `heatmap.js`, from where it was loaded itself (or from odo, when that path doesn’t have it), never from an address in the link. Every other page view goes without it. Closing the panel takes the token out of the address bar.

### If your site has a Content-Security-Policy

The overlay is a script from odo that asks odo for the heatmap, so a policy that limits scripts or connections has to allow odo’s address in both:

```
script-src 'self' https://odo.gg;
connect-src 'self' https://odo.gg;
```

odo.js needs the same to load and send visits, so where it measures the page the overlay usually works too; a policy that allows odo.js by its full path (`https://odo.gg/js/odo.js`) needs `https://odo.gg` as well. The overlay sets its styles from its script, so `style-src` needn’t change, and a nonce on odo.js’s tag is passed on to it. When the page opens without the heatmap, the sheet says so after about 25 seconds, and the panel names what to allow when it can’t reach odo.

### Served from your own domain

If odo.js loads from your own domain (see [ad blockers](https://odo.gg/docs/add-the-script#ad-blockers)), the overlay and the heatmap it draws come from the same path: with everything under `/odo/` forwarded to odo, that’s `/odo/js/heatmap.js` and `/odo/api/heatmap`, and the policy needs nothing more than `'self'`.

If your proxy forwards only `/odo/js/odo.js` and `/odo/api/event`, the heatmap still opens: the overlay and its heatmap come from `https://odo.gg` itself, so a policy has to allow it as above. Forward the whole path to keep them on your domain.

### Apps that route with the hash

The link uses the page address’s `#` for itself, so pages of an app that routes with `#/paths` (`data-hash` on the tag) can’t be opened with their heatmap over them. Their heatmaps are on the dashboard and for assistants all the same.

## Reading it

**Clicks.** Many clicks on something that isn’t a link or a button (an image, a price, a heading) mean people expect it to do something: make it one. Many clicks on a secondary link and few on the main button mean the call to action isn’t standing out. Rage clicks say something looks broken or slow; dead clicks, that something looks clickable but isn’t.

**Scroll.** Half of visitors stop above the fold line, so whatever sits below it is seen by fewer than half. Compare phones with desktops: the same page is taller on a narrow screen, and the fold falls somewhere else.

**Attention.** A part that matters (pricing, a signup form, the main call to action) seen by fewer than half of visitors sits below where most stop: move it up, or point to it from the top. A part most visitors see but keep in view for only a second or two is skimmed past.

A heatmap shows what people did, not what would do better. To find that out, test the change as an [experiment](https://odo.gg/docs/experiments).

### When most leave before the part that sells

odo reads the heatmaps of your eight busiest pages over the last 30 days. When one with at least 100 visitors with heatmaps (and at least 5% of the site’s visitors) has a key part that fewer than half of them reach, **Next moves** says so: *Most visitors stop before “Pricing” on /pricing*, with how many scrolled as far as it and where half had stopped. Tap it for the page’s heatmap. A key part is one whose name or selector has a word like *pricing*, *plan*, *form*, *signup*, *checkout*, *cart*, *buy*, *book*, *contact*, *demo*, *trial*, *get started* or *cta* in it, and only the page and part that the most visitors miss is raised at a time.

Naming your parts with `data-odo-section` (*Pricing*, *Signup form*) is what lets odo tell which of them sells.

## What’s never recorded

- Nothing anyone types, and nothing inside `data-odo-ignore`: a click there isn’t placed at all.
- A click is placed by where it is, never by what it says: a path of tags, and of ids and classes that read as written by hand. Class names a build tool makes up (`css-1x2y3z`), ids with long numbers (`order-58213`), emails and long random strings are left out, by the script and again when odo receives them, and a path is kept to 120 characters.
- Only a link, button or field is named, by its own label, as [clicks](https://odo.gg/docs/interactions) are. Anything else is only where it is.
- The window’s width is kept only as phone, tablet or desktop.
- Heatmaps don’t need the visitor id: cookieless sites, and visitors whose browser sends Global Privacy Control, are measured the same way.
- What comes back out is summed up: counts per element, part and depth, never a visit or a visitor.

## With an AI assistant

An assistant [connected to odo](https://odo.gg/docs/mcp) reads the same heatmaps with three tools. Each reads one page by its path (`/pricing`, no wildcards), on `phone`, `tablet`, `desktop` or `all` screens, in the range and filters the other traffic tools take.

| Tool | What it answers |
| --- | --- |
| `page_heatmap` | What’s clicked, most first: links and buttons by their label, anything else with a selector to find it in your code; clicks, clickers, share of the page’s clicks, rage and dead clicks |
| `scroll_depth` | The share who reach every 5% of the page, the fold, and how many screens tall it is |
| `page_attention` | The page’s parts in order, the share who saw each, and its median time in view |

```prompt
Over the last 30 days, compare the heatmaps of my home page (/) on phones and on desktops with odo: how far down half of visitors get on each, which parts of the page sit below that point, and what gets clicked that isn’t a link or a button. Tell me the biggest difference between the two screens, with the numbers it rests on, and one change to test for phones.
```

The tools’ inputs are in the [tool reference](https://odo.gg/docs/mcp-tools#heatmaps-scrolling-and-attention), and more to copy in [prompts to try](https://odo.gg/docs/prompts).
