If you've opened a Matomo heatmap and found a blank rectangle where your hero image should be, or a product grid that cuts off after the first row, or a sticky header sitting square on top of the page content, you're not the first. Heatmap screenshots break on most modern websites out of the box. The reason is structural, not something you fix by tweaking your tracking config.
Heatmap screenshots break on most modern sites, and it's a snapshot-and-replay problem, not a tracking one. Matomo stores the page structure and rebuilds it later, so cross-origin CSS, unstored images, and clipped or sticky elements fall apart in the preview even though your click and scroll data is fine. Matomo Heatmap Helper is an open source Chrome extension that handles all of that at capture time and then restores your live page. The part that earns its keep is Interactive mode, where you click the elements that misbehave so the extension knows what to fix; it needs Heatmap & Session Recording 5.1.0 or later and a Matomo API token with write access.
Matomo captures an initial page snapshot from the browser and stores the page structure for later heatmap viewing. CSS and images are not always stored with that snapshot; they may still be loaded from your site or CDN when Matomo reconstructs the page. Lazy content may not exist yet when the snapshot is taken. Viewport-dependent CSS may replay differently. A lot of things that worked because they were running in the original browser quietly fail when the heatmap viewer rebuilds the page later.
Once you see that, the bug list starts making sense.
The patterns we keep running into
Most sites hit at least three of these. Some hit all of them.
| Symptom | Likely cause | Permanent fix | What the extension does |
|---|---|---|---|
| Missing styling, icons, or fonts | Cross-origin CSS or font files cannot be read during capture or replay | Add crossorigin="anonymous" on stylesheet links and return CORS headers for CSS/font files | Fetches CSS and font resources with extension permissions and embeds what it can as data URIs |
| Broken hero or product images | Matomo does not store images, so blocked, moved, lazy, or origin-sensitive image URLs can disappear later | Keep image URLs stable, make them absolute, and make sure they are loaded before capture | Resolves image URLs and embeds loaded images into the snapshot |
| Product grid or carousel cut off | Fixed-height overflow containers clip content even when it exists in the DOM | Add capture-only CSS that expands the container for heatmaps | Expands selected scroll containers before capture, then restores them |
| Header covers the whole page | Sticky/fixed headers, parallax sections, or 100vh wrappers replay differently | Scope heatmap-only CSS with html.matomoHeatmap and set explicit heights or static positioning | Converts selected sticky/fixed elements to normal flow during capture |
| Assets referenced with relative URLs fail | URLs such as /images/hero.jpg are replayed outside the original page context | Use absolute URLs for heatmap-critical assets | Rewrites relative URLs to absolute URLs before capture |
| SPA or lazy content missing | Matomo's automatic snapshot ran before React, Vue, or lazy assets finished rendering | Use Matomo's manual snapshot capture after the page is actually ready | Lets you wait, interact with the page, then trigger the manual snapshot |
Most of these have infrastructure-level fixes. Add CORS headers on your CDN for CSS and font files. Switch important assets to absolute URLs. Use the html.matomoHeatmap CSS hook to override sticky positioning or height rules only during heatmap rendering. Matomo's own docs cover several of them, and we've written a longer post that walks through each issue and how to fix it server-side if that's the route you want.
The catch is that the work spreads across CDN config, CSS, and your tracking setup, and some of it you can't fix at all if you don't control the assets. We hit that wall on enough client sites that we ended up doing something else.
What we ended up building
We started with a small script we'd paste into DevTools before each capture. It embedded images and fonts as data URIs, expanded scroll containers, unstuck headers, rewrote relative URLs to absolute. Over time it grew into a Chrome extension we used internally on every client.
That's what Matomo Heatmap Helper is. We open sourced it.
Setup is one screen. You enter your Matomo URL, an API token with write access to the target site, and pick which sites you want the toolbar to appear on. On any matching page, a small bar shows up.
| Requirement | Why it matters |
|---|---|
| Heatmap & Session Recording enabled | The extension uses Matomo's heatmap capture machinery; it does not add heatmaps to Matomo by itself. |
| Manual snapshot capture available | Matomo added the explicit manual snapshot workflow in Heatmap & Session Recording 5.1.0. |
| Token with write access | The extension reads sites and heatmaps, updates the heatmap into manual capture mode, and verifies that a new snapshot exists. |
| A browser where the extension can run | The current extension targets Chrome and other Chromium-based browsers. |
| A page Matomo can track | The Matomo tracker and Heatmap & Session Recording plugin still need to load on the page. If your IP is excluded in Matomo, Matomo's manual capture will not work until that exclusion is removed temporarily. |
The work happens at capture time. When you press the screenshot button, the extension does the things that are time consuming to do statically. It embeds images and fonts as base64 data URIs where possible, so the heatmap snapshot is not depending on those assets being fetched again later. It expands scroll containers to their full content height. It converts sticky and fixed headers to normal flow. It rewrites relative URLs to absolute, resizes iframes, pauses videos to frame zero, and gives SPA content time to render. Then it enables Matomo's manual snapshot mode for the selected heatmap, triggers Matomo's HeatmapSessionRecording::captureInitialDom tracker command in the page, checks that Matomo received the result, and reverses every change. Your live page goes back to how it was.
The reason this works is that you can interact with elements before capture. Custom scroll containers, a misbehaving sticky header, a section where assets aren't loading: you click them in Interactive mode, they get a lock icon, and the pipeline knows what to do with them. The tracking script alone can't do that, because it captures whatever state the DOM happened to be in at the moment it ran. Letting a person tell the extension which elements need handling is what fixes the cases the script never could.
Issues and pull requests are welcome.
Why we put it out
As an agency that works deeply with Matomo, broken heatmaps were a problem we hit on almost every client engagement. The events were tracked correctly, but the screenshots were unusable, which is the part most stakeholders actually look at.
Matomo has given us a lot as an agency. We use the extension daily on client work, and we keep improving edge cases we find on client websites. If you run into an unresolved heatmap screenshot issue, open a GitHub issue with the affected URL pattern, Matomo version, browser, and a screenshot of the failure.
What we're working on
Matomo Heatmap Helper grew out of a larger project. Martez connects Matomo with Meta Ads and Google Ads, so ROAS, CLV, and multi-touch attribution sit next to your web analytics instead of in a separate spreadsheet. It's in private beta. If that's relevant to you, you can join the waitlist.
Either way, the heatmap extension is its own thing, and it stays that way. We built it because we needed it.