If you've opened a Matomo heatmap and found your sidebar truncated, the bottom half of a modal missing, or click markers floating over content that doesn't seem to belong to them, the clicks themselves are usually fine. What's broken is the screenshot underneath them. Some wrapper on the page has overflow: hidden, overflow: auto, or overflow: scroll set, and Matomo's renderer treats whatever sits past that wrapper's visible edge as if it isn't there. So the heatmap shows you a slice of the page instead of the page your visitors actually saw.
The fastest fix is a free Chrome extension we maintain called Matomo Heatmap Helper. It neutralizes overflow containers right before each capture, lets the renderer see the full document, and restores the original styles afterward. The rest of this post is how to do the same thing without an extension, plus a couple of permanent fixes worth shipping.
A CSS overflow container on the page (overflow: hidden/auto/scroll, or a fixed height) is clipping the DOM Matomo captured, so the screenshot only shows the visible slice. If you control the stylesheet, scope an override to Matomo's html.matomoHeatmap renderer class so only the screenshot changes, not your live page. One catch: editing the CSS doesn't touch a screenshot Matomo already stored, so delete the stale one and recapture before you read anything into the clicks.
How to fix it without the extension
There's a console snippet that papers over the problem right before each capture, and a few permanent fixes you can ship with the site. Pick whichever fits the constraints you're working under.
List every overflow-restricted element on the page
Before you change anything, find out which wrappers are clipping. Open the page in Chrome, hit F12 to open DevTools, switch to the Console, and paste:
// Lists every element that's actually clipping content right now.
const clipping = ['hidden', 'auto', 'scroll', 'clip'];
Array.from(document.querySelectorAll('*')).filter(el => {
const s = getComputedStyle(el);
const clips = clipping.includes(s.overflowX) || clipping.includes(s.overflowY);
const hasHiddenContent =
el.scrollHeight > el.clientHeight + 1 || el.scrollWidth > el.clientWidth + 1;
return clips && hasHiddenContent;
});That filters on both axes (overflow-x and overflow-y, plus the newer overflow: clip) and only keeps elements whose content actually overflows their box, so you don't get a list of every harmless container on the page. The usual culprits are sidebars, modals, custom scroll wrappers around <main>, and old clearfix wrappers carrying overflow: hidden for layout reasons that stopped mattering years ago.
The quick fix: strip overflow restrictions from the console
This is the snippet we paste into the browser console right before triggering a capture. It walks every clipping element and flips its overflow to visible, then lifts any fixed height that was doing the real clamping. By the time Matomo serializes the DOM, nothing is hiding past a visible edge.
// Paste into the console right before you trigger the capture.
// Turns every clipping container on the page into 'visible'.
const clipping = ['hidden', 'auto', 'scroll', 'clip'];
document.querySelectorAll('*').forEach(el => {
const s = getComputedStyle(el);
if (
clipping.includes(s.overflow) ||
clipping.includes(s.overflowX) ||
clipping.includes(s.overflowY)
) {
el.style.overflow = 'visible';
el.style.maxHeight = 'none';
el.style.height = 'auto';
}
});The page is going to look strange for a moment. Sidebars expand to their full content height, modals stretch off the edge of the viewport, and anything that depended on a fixed-height container suddenly doesn't have one. That's fine. You're capturing the heatmap, not browsing the site. Refresh when you're done.
That's the quick path. It works for one-off captures and the review cycles where you need a clean screenshot now and don't want to wait on a code change. For anything you'll re-capture regularly, ship one of the permanent fixes below.
Permanent CSS fix scoped to Matomo's renderer
When you control the stylesheet, the clean answer is an override that only applies while Matomo is rendering. Matomo adds a class to the <html> element when it renders these views: matomoHeatmap for heatmaps, and matomoHsr for either a heatmap or a session recording. Prefix your selectors with one of those and the override exists for the screenshot and nowhere else.
/* Edit the selectors to match the wrappers on your site. */
html.matomoHeatmap .sidebar,
html.matomoHeatmap main,
html.matomoHeatmap .modal-shell {
overflow: visible !important;
overflow-y: visible !important;
height: auto !important;
max-height: none !important;
}No JavaScript, no class to toggle yourself. Matomo puts the class there only while it builds the screenshot, so your visitors keep the layout you designed for them and the renderer sees the full document. (Matomo documents these renderer prefixes in its custom-stylesheet FAQ.) If you're driving a capture by hand in a browser instead, you can gate the same CSS on a query param like ?heatmap=1 rather than the renderer class.
No, as long as you scope it to html.matomoHeatmap (or html.matomoHsr). A tempting shortcut is to gate the override on if (window._paq), but _paq is present for every tracked visitor on every page where Matomo loads, so that would strip overflow from your live layout for real traffic. The renderer class only exists during capture, which is exactly the window you want.
After you change anything, recapture the screenshot
Editing the CSS doesn't touch a screenshot Matomo already stored. An active heatmap retakes its screenshot the next time a visitor loads the page, so sometimes you just wait. To force it, delete the existing screenshot from the heatmap's settings and let Matomo recapture on the next visit, or grab a fresh snapshot yourself. In Matomo 5.1 and up the heatmap UI shows a manual command you can paste into the console once the page is fully rendered:
_paq.push(['HeatmapSessionRecording::captureInitialDom', { idHeatmap: 1 }]);Then reopen the heatmap and check the background image before you read anything into the click data. It's easy to ship a correct CSS fix and still be staring at the old, broken screenshot.
Drop the nested scroll container entirely
If your page has overflow: auto on <main> or some other wrapper for "scrollytelling" reasons, the architectural fix is to drop it and let the document scroll. Matomo's renderer can reproduce document scroll; it can't reproduce a nested scroll position. You get better mobile behaviour out of it too (iOS Safari has its own opinions about nested scroll containers), and keyboard scroll and screen-reader navigation start working the way users expect.
It's a bigger change than the CSS override, but it's the one that stops the problem from coming back the next time someone adds a section to the layout.
Replace clearfix overflow: hidden with display: flow-root
Old clearfix wrappers carrying overflow: hidden were a workaround for containing floated children. display: flow-root does the same containment without clipping anything, and it's been safe across every browser worth supporting since 2018.
/* Edit the selector to match your clearfix wrapper. */
.row-with-floats {
display: flow-root;
}The quick way to find candidates is to grep your CSS for overflow: hidden and check whether each one is actually meant to clip anything. Most of the time, no. One caveat: this only swaps out overflow: hidden that was containing floats. Where the design genuinely clips on purpose (a drawer, a carousel, an animated accordion), flow-root isn't the fix, and you want the scoped renderer override instead.
A note on the trade-off
Whichever permanent fix you pick, scope the override (a renderer class, a query param, a heatmap-only stylesheet) instead of changing the live styles for everyone. Your visitors are seeing the layout you built for them, and the modal that's supposed to scroll inside its own box should keep doing that. The override only needs to exist for the renderer Matomo points at the page.
Why Matomo can't render your overflow containers
Matomo's screenshot capture happens in two steps. The tracker serializes your live DOM into HTML and sends it to your Matomo server, which saves the page structure. When you open the heatmap, Matomo re-renders that saved markup to produce the background you see. It isn't your visitor's browser session being replayed. It's a stored copy of the HTML being rendered fresh, without the scroll position or in-browser state the visitor had at the time.
- 1
Visitor browser
the live DOM
- tracker serializes the HTML2
Matomo stores the page structure
- re-rendered with html.matomoHeatmap3
Screenshot background
- click data plotted on top4
The heatmap you read
That renderer can reproduce document scroll, which is why long pages capture fine. What it can't reproduce is the inner scroll position of a nested overflow: auto container, because that lived in the visitor's browser and was never part of the serialized markup. Where a wrapper has overflow: hidden, the renderer clips exactly like any browser would. The screenshot ends up showing the visible slice of that container, with everything past its edge gone.
The click markers can make this look worse than it is. Modern Matomo records interactions relative to the elements they hit rather than as fixed document pixels (the Heatmap & Session Recording plugin has told window scrolling apart from in-element scrolling since version 3.1.8), so the click data itself is usually sound. What's wrong is the picture underneath it. If the stored screenshot shows a scroll container at the top while the visitor clicked something further down inside it, the marker lands over whatever happens to occupy that spot in the screenshot, and it looks like the click missed. Fix the screenshot first, then decide whether the clicks actually look off.
What's actually failing
A few patterns keep coming up. Most are some variation of "a box has a fixed size and clips whatever doesn't fit."
| Pattern | What you see in the heatmap | The CSS behind it |
|---|---|---|
| Scrollable sidebar or modal | Only the part visible at capture time | Fixed height + overflow: auto on the wrapper |
"Scrollytelling" <main> | One viewport's worth of content | overflow: auto/scroll on a wrapper instead of document scroll |
| Legacy clearfix wrapper | Content below the fold dropped | overflow: hidden that was only meant to contain floats |
| Animated dropdown or accordion | The closed, clipped state | overflow: hidden plus a height transition |
| Masked or clipped container | Same clipping, different property | mask or clip-path on a container |
If your heatmap is missing content or the markers don't line up with the elements you'd expect, you're hitting at least one of these, often more than one on the same page. The console snippet and the renderer CSS reach most of them; what they can't reach is content inside a cross-origin iframe or a closed shadow root, which is where the extension (or accepting the limitation) comes in.
This is the same family of problem that breaks fonts, images, and sticky headers in the same screenshot. We've written a longer post on broken Matomo heatmap screenshots that walks through the rest, plus separate posts on why fonts aren't loading and why images aren't loading, since those come up almost as often.
What we'd actually do
If you control the stylesheet, ship the renderer-scoped override. One CSS block prefixed with html.matomoHeatmap is the smallest diff that solves the problem permanently and it doesn't touch what visitors see. Then delete the stale screenshot and recapture so you're looking at the fixed background.
If the layout has a nested scroll container that's been bugging you for other reasons too, take the architectural fix and let the document scroll. You'll save yourself this problem on every future capture, and your mobile users will thank you for it.
If neither is reachable (no stylesheet access, third-party widgets you can't restyle, a CMS that won't let you ship custom CSS), the Matomo Heatmap Helper Chrome extension is what we use on client sites where we can't change the stack. It walks the same overflow containers the snippet finds, neutralizes them for the capture, and restores them after, so the page is intact for the next visitor. Free, open source, code on GitHub.
Martez is the larger project the extension came out of. It connects Matomo with Meta Ads and Google Ads so ROAS, CLV, and attribution sit next to your web analytics instead of in a separate spreadsheet. It's in private beta. Join the waitlist if that's relevant to you.
Most of the time, this is a stylesheet change away. Worth doing once.