Matomo heatmap screenshots usually break because the saved page snapshot depends on things that are fragile later: readable CSS, reachable images, font files, stable layout CSS, and content that finished rendering before the snapshot was captured.
Matomo doesn't store a flat picture of your page. It saves the DOM and rebuilds it later, still pulling CSS, fonts, and images from your live site, so assets that are missing, served over plain HTTP, or blocked by CORS fail in the reconstruction while the real page looks fine. Open DevTools in the heatmap view to see which requests fail, and fix CSS/font CORS and missing assets at the source first. Use html.matomoHeatmap CSS or Matomo 5.1+ manual snapshot capture only for layout and SPA timing problems.
When the first matching page view is captured, Matomo's browser-side tracker saves the page DOM to Matomo. Later, the heatmap UI reconstructs that DOM and overlays clicks, moves, and scroll data. Matomo's own docs describe CSS and images as separate caveats: styling and resources may still need to be read, saved, or loaded from the original website when you view the heatmap.
That is the gap. Your tracking data can be correct while the visual background is wrong.
What broken heatmap screenshots look like
These are the failures people typically run into:
| Symptom | Likely cause | Confirm | Fix |
|---|---|---|---|
| Hero image is a broken icon | Missing file, relative URL, HTTPS mismatch, auth/hotlink protection, or an inlining script blocked by CORS | Open DevTools while viewing the heatmap and inspect failed image requests in the reconstructed page frame | Restore or expose the asset, use HTTPS, prefer absolute URLs, or inline only critical images during capture |
| Fonts or icon fonts fall back | Matomo cannot read or load CSS/font files from the stylesheet host | Look for CORS errors or failed CSS/font requests in Console and Network | Add crossorigin="anonymous" to stylesheet links and scope CORS headers to CSS/font assets |
| Scrollable content is cut off | The saved DOM still carries fixed height, max-height, or overflow CSS | Inspect the clipped element and compare scrollHeight with clientHeight | Use heatmap-only CSS or capture-time changes to expand the container |
| Sticky nav covers the page | Fixed, sticky, parallax, or 100vh layout CSS anchors to the reconstructed heatmap viewport | Toggle the element's position, top, and height rules in DevTools inside the heatmap frame | Use html.matomoHeatmap CSS to neutralize the specific header or section |
| Samples exist but no screenshot | The initial DOM snapshot request was blocked or became too large | With DevTools open, trigger a fresh capture and look for 403, 414, or 500 responses | Check WAF rules, security plugins, POST-size limits, and Matomo server logs |
| SPA content is half missing | The automatic snapshot ran before route data, lazy sections, or images settled | Compare the heatmap snapshot timing with route and network completion in DevTools | Use Matomo 5.1+ manual snapshot capture after the page is ready |
Hero image is a broken icon
- Cause:
- Missing file, relative URL, HTTPS mismatch, auth/hotlink protection, or an inlining script blocked by CORS
- Confirm:
- Open DevTools while viewing the heatmap and inspect failed image requests in the reconstructed page frame
- Fix:
- Restore or expose the asset, use HTTPS, prefer absolute URLs, or inline only critical images during capture
Fonts or icon fonts fall back
- Cause:
- Matomo cannot read or load CSS/font files from the stylesheet host
- Confirm:
- Look for CORS errors or failed CSS/font requests in Console and Network
- Fix:
- Add crossorigin="anonymous" to stylesheet links and scope CORS headers to CSS/font assets
Scrollable content is cut off
- Cause:
- The saved DOM still carries fixed height, max-height, or overflow CSS
- Confirm:
- Inspect the clipped element and compare scrollHeight with clientHeight
- Fix:
- Use heatmap-only CSS or capture-time changes to expand the container
Sticky nav covers the page
- Cause:
- Fixed, sticky, parallax, or 100vh layout CSS anchors to the reconstructed heatmap viewport
- Confirm:
- Toggle the element's position, top, and height rules in DevTools inside the heatmap frame
- Fix:
- Use html.matomoHeatmap CSS to neutralize the specific header or section
Samples exist but no screenshot
- Cause:
- The initial DOM snapshot request was blocked or became too large
- Confirm:
- With DevTools open, trigger a fresh capture and look for 403, 414, or 500 responses
- Fix:
- Check WAF rules, security plugins, POST-size limits, and Matomo server logs
SPA content is half missing
- Cause:
- The automatic snapshot ran before route data, lazy sections, or images settled
- Confirm:
- Compare the heatmap snapshot timing with route and network completion in DevTools
- Fix:
- Use Matomo 5.1+ manual snapshot capture after the page is ready
Each one has a specific cause. The rest of this post takes them in turn.
CSS and font CORS
This is the first thing to check when the heatmap looks unstyled, icon fonts disappear, or brand fonts fall back to system defaults.
Matomo's CORS guidance is mainly about CSS access. When CSS comes from another host, Matomo may need to read it from the DOM or fetch it separately so the reconstructed page keeps its layout. If the browser blocks that access, the heatmap UI can lose styles and fonts even though your live site looks fine.
Open DevTools while viewing the heatmap or session replay. If the Console or Network tab shows a CORS error for a stylesheet or font file, fix that host, not the Matomo tracking snippet.
For CSS and fonts, add crossorigin="anonymous" to stylesheet links and configure the stylesheet or font host to return an Access-Control-Allow-Origin header for the origin shown in the browser's CORS error. Keep this limited to CSS and font files. Matomo explicitly warns against applying broad CORS headers to every resource.
<link rel="stylesheet" href="https://cdn.example.com/app.css" crossorigin="anonymous">Access-Control-Allow-Origin: https://www.example.comMatomo also supports data-matomo-href for cache-busted CSS that it cannot read from a CDN. Keep the hashed file for the live page, and point Matomo at a stable equivalent:
<link href="style.223ks2j3.css" data-matomo-href="style.css">Matomo's docs cover both the missing styling cases and the CORS setup path.
Images that cannot be loaded or inlined
Broken images are related, but they are not always a CORS problem.
Matomo's docs say CSS and images are not simply stored as part of the snapshot forever. They may still be loaded from your website when you view the heatmap or replay the recording. That means a hero image can fail because the file was removed, a signed URL expired, hotlink protection rejected the request, HTTPS is incomplete, a browser extension blocked it, or the URL was relative and resolves incorrectly in the reconstructed page.
Use DevTools in the Matomo heatmap view. The failed request URL tells you which class of problem you have:
404or410: restore the asset or recapture after deploying current assets401or403: remove auth/hotlink restrictions for the heatmap path or use a capture-time workaroundhttp://under an HTTPS Matomo UI: fix mixed content and serve the asset over HTTPS- a request under the Matomo domain for a site asset: rewrite relative URLs before capture or use absolute URLs
Scroll containers cutting off your content
You have a section with a fixed height and a scrollbar. In the heatmap screenshot, only the visible portion renders. Everything below the fold is gone.
The saved DOM still contains the overflow container and its CSS. When Matomo reconstructs the page, that container keeps its fixed height, max-height, and overflow behavior, so only the visible part paints.
To fix it permanently, avoid fixed heights on content containers. Use min-height instead of height, and don't apply overflow: hidden on elements that contain content you need in the heatmap.
For a heatmap-only fix, target the reconstructed page:
html.matomoHeatmap .your-scroll-container {
height: auto;
max-height: none;
overflow: visible;
}Before a manual capture, you can also temporarily expand containers where scrollHeight > clientHeight, capture the snapshot, then restore the original styles.
Sticky and fixed headers overlapping the page
Your sticky navigation works perfectly in the browser, but in the screenshot it just sits on top of the content, covering whatever is underneath.
During heatmap reconstruction, fixed, sticky, parallax, and 100vh elements can anchor to the heatmap viewport or stretch across the reconstructed page. The result is a header or hero section covering content that should be visible underneath.
Matomo adds helper classes to the <html> element during heatmap and session-recording display. Use html.matomoHeatmap for heatmaps only, html.matomoSessionRecording for session replays only, or html.matomoHsr for both. For example:
html.matomoHeatmap .your-sticky-header {
position: relative;
top: auto;
bottom: auto;
}This converts your sticky header to normal flow in the screenshot without affecting how the page looks for real users. Matomo's docs cover this in their FAQ on fixing overlapping headers in heatmaps and applying custom stylesheets during capture.
If the problem is a full-height hero or parallax section, cap that element for heatmaps instead of changing the header:
html.matomoHeatmap .hero {
max-height: 900px;
}If you'd rather not maintain those CSS rules, the capture-time approach is to detect header and nav elements with fixed or sticky positioning, convert them to normal flow, clear their inset values, and for fixed elements, insert a placeholder div to keep the layout from shifting. After capture, everything gets restored.
Relative URLs that stop working
Some images and stylesheets load fine on your site but are missing in the Matomo screenshot, even though they're not cross-origin. This one is confusing until you realize what's going on.
Your page uses relative URLs like /images/banner.jpg. In the live browser, that resolves against your website. In the reconstructed heatmap view, inspect the failed request before assuming the same base URL still applies. If the request is going to the Matomo domain, a CDN cache path, or another unexpected base path, the saved markup is carrying a relative URL that no longer resolves where you need it.
The durable fix is to use absolute asset URLs in captured markup. If neither templates nor CMS output are easy to change, a pre-capture script can scan src, href, srcset, poster, SVG references, and CSS background-image values and rewrite them to absolute URLs using the page's current origin.
You can also inject a <base href="https://yoursite.com/"> tag for capture, but treat that as a temporary capture-time change. A permanent <base> tag can alter links, forms, scripts, and analytics behavior, so verify it carefully.
Custom fonts showing up as system defaults
Your brand fonts are gone. The screenshot shows whatever system font the server has, which is usually something ugly.
@font-face rules reference font files hosted on CDNs or your server. If Matomo cannot read the CSS or the reconstructed page cannot load the font files, the browser falls back to a default font.
Start with the CSS/font CORS fix above: crossorigin="anonymous" on the stylesheet link, narrow CORS headers on CSS and font files, and a quick DevTools check in the heatmap UI. If changing headers is not an option, you can embed critical fonts directly in the page for capture by fetching the font files, converting them to data URIs, and injecting temporary @font-face rules into a <style> tag.
Keep font inlining focused. Font files are large, and over-inlining can make the initial snapshot request big enough to hit the same WAF or POST-size problem that causes "samples exist but no screenshot."
Iframes, videos, and SPAs
Three more cases that don't fit the patterns above, but come up often enough to plan for.
Iframes often have fixed CSS dimensions that don't match their actual content. For same-origin iframes you can read the real content height from contentDocument.body.scrollHeight and resize accordingly. For cross-origin iframes, you'll need a generous fallback height.
Auto-playing videos get captured mid-playback, showing a random frame. Pause them and seek to frame 0 before capture so the screenshot shows the first frame.
Single page applications need timing control. Matomo's Heatmap & Session Recording tracker supports SPAs out of the box when your app calls trackPageView on route changes. If you use trackPageView for non-page-view events or need explicit route control, use HeatmapSessionRecording::disableAutoDetectNewPageView and then HeatmapSessionRecording::setNewPageView when the route really changes.
For Matomo Heatmaps 5.1.0 and newer, the precise fix for late-rendering content is manual snapshot capture. Enable Capture Heatmap Snapshot Manually in the heatmap configuration, wait until the route, data, images, and lazy sections have settled, then run the command Matomo gives you.
_paq.push(['HeatmapSessionRecording::captureInitialDom', { idHeatmap }])Run this in the browser after the page is complete. If your IP address is excluded in Matomo settings, remove that exclusion temporarily before testing manual capture.
Fixing everything at once with Matomo Heatmap Helper
Most real sites have several of these problems simultaneously. Fixing them all manually means touching CDN configs, rewriting URLs, refactoring CSS, and writing custom capture scripts.
Matomo Heatmap Helper is a free, open source extension for Chrome, Edge, and other Chromium-based browsers that handles all of the above. Source on GitHub.
Here's how it works in practice. You install the extension, enter your Matomo URL and API token, and pick which sites to enable. On matching pages, a toolbar appears. Click "Interactive" and then click the elements that need fixing: scroll containers, sticky headers, sections with missing images. They get a lock icon to show they'll be processed.
When you hit the screenshot button, the extension runs through a sequence. It fetches and embeds selected images and fonts first, then expands constrained elements and unsticks headers, handles iframes and videos, converts relative URLs to absolute, and triggers Matomo's initial DOM/snapshot capture for the selected heatmap. After capture, every change is reversed. Your page goes back to how it was.
Choosing the right fix
| Your situation | Least invasive fix |
|---|---|
| You can change CSS or CDN settings | Fix it at the source: CORS on CSS and fonts, HTTPS everywhere, absolute URLs, heatmap-only CSS overrides |
| You can't, but you control when the screenshot is taken | Matomo 5.1+ manual snapshot, triggered after the page has fully rendered |
| Neither (locked CMS, client site, third-party scripts) | A capture-time helper in the browser (the extension above) |
Whichever row you land on, finish the same way: delete or recapture the old snapshot, then verify the result in the heatmap UI.
Before you take a screenshot
A quick checklist:
- Open DevTools before the capture and keep Console and Network visible
- Make sure the page has fully rendered, especially SPA content and lazy loaded sections
- Lock containers that clip content or need expansion
- Check for sticky, fixed, parallax, or 100vh sections that might overlap content
- Verify key images, CSS, and fonts load without CORS, HTTPS, or auth errors
- Take the snapshot and confirm the initial DOM capture request succeeds
- Open the heatmap view and inspect the result before starting analysis
Two paths forward
These screenshot issues usually are not bugs in your analytics data. They are side effects of saving a page snapshot and reconstructing it later with live CSS, resources, and layout rules still in play. Any site using CDNs, web fonts, scroll containers, lazy rendering, or sticky headers can hit some combination of them.
You can fix each issue at the infrastructure level: scope CORS headers for CSS and fonts, use absolute URLs, keep assets available over HTTPS, refactor overflow containers, and add html.matomoHeatmap CSS for sticky or full-height sections. That's the right long-term approach if you control the server and templates.
Or you can fix them at capture time with Matomo Heatmap Helper. Use the interactive mode to tell it which parts of your page need help, and get clean screenshots without touching production code.