hotjarwebflowanalyticslanding-pages

Hotjar Single Page Application Setup: Webflow & SPA Guide

Published June 22, 2026

Hotjar Single Page Application Setup for Webflow and SPAs

If you installed Hotjar single page application tracking and the recordings look broken, the data is incomplete, or page views are missing, you are not alone. Hotjar was built for traditional multi-page sites. SPAs change that contract, and Webflow has its own quirks on top.

This guide walks through what breaks, why it breaks, and the exact setup to get accurate session recordings and heatmaps on Webflow sites, React/Vue apps, and hybrid setups.

Why Hotjar struggles with SPAs by default

A single page application doesn't reload the browser when users navigate. Routes change via JavaScript. The URL updates, content swaps, but the page itself never refreshes.

Hotjar's default tracking script fires on page load. One load equals one tracked page. So when a user clicks through five "pages" in your React app, Hotjar sees one session on one URL. Heatmaps stack clicks from different views onto the same image. Recordings show route changes but the analytics treat it all as a single page view.

This causes three concrete problems:

  1. Heatmaps mix data from different views. A click on "/pricing" gets recorded against "/" if that's where the session started.
  2. Page-specific filters break. You can't filter recordings by URL because Hotjar only logged the initial URL.
  3. Funnel analysis gives garbage. Drop-off rates between routes are invisible.

The fix: manual virtual page view triggers

Hotjar exposes a stateChange method specifically for SPAs. You call it whenever the route changes. This tells Hotjar to start a new page view, attribute subsequent activity to the new URL, and rebuild heatmaps correctly.

The base call looks like this:

window.hj('stateChange', '/new-route');

Where you put this call depends on your framework. Let's go through the common ones.

Webflow setup

Webflow is technically multi-page, so most users skip the SPA setup. But there are two scenarios where you need it:

Scenario 1: Webflow with custom JavaScript routing. Some teams build interactive product pages or onboarding flows in Webflow using client-side state changes. If you're swapping content with JS instead of linking to new pages, Hotjar needs help.

Scenario 2: Webflow with embedded SPAs. You have a marketing site on Webflow and an embedded app (React widget, signup flow, configurator) that changes state without navigation.

For both cases, add the standard Hotjar tracking snippet to your site-wide custom code (Project Settings → Custom Code → Head Code). Then, wherever your custom script triggers a content change, add:

if (window.hj) {
  window.hj('stateChange', window.location.pathname);
}

If your Webflow site is fully static and uses normal page links, ignore all this. Your default install works fine. The issue I see most often is teams overengineering Webflow installs because they read SPA guides without checking if they actually have an SPA.

React setup with React Router

For React apps using React Router v6, hook into route changes with useLocation:

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

export function HotjarRouteTracker() {
  const location = useLocation();
  
  useEffect(() => {
    if (window.hj) {
      window.hj('stateChange', location.pathname + location.search);
    }
  }, [location]);
  
  return null;
}

Drop <HotjarRouteTracker /> inside your <BrowserRouter> and route changes get tracked automatically. Include location.search if you care about query parameters (UTMs, filters, etc.).

Next.js setup

Next.js needs different handling depending on whether you're on the Pages Router or App Router.

Pages Router:

import { useRouter } from 'next/router';
import { useEffect } from 'react';

useEffect(() => {
  const handleRouteChange = (url) => {
    if (window.hj) window.hj('stateChange', url);
  };
  router.events.on('routeChangeComplete', handleRouteChange);
  return () => router.events.off('routeChangeComplete', handleRouteChange);
}, [router.events]);

App Router:

'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';

export function HotjarTracker() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  
  useEffect(() => {
    const url = pathname + (searchParams.toString() ? `?${searchParams}` : '');
    if (window.hj) window.hj('stateChange', url);
  }, [pathname, searchParams]);
  
  return null;
}

Wrap the App Router version in <Suspense> because useSearchParams requires it.

Vue setup with Vue Router

Vue Router exposes afterEach for this exact purpose:

router.afterEach((to) => {
  if (window.hj) {
    window.hj('stateChange', to.fullPath);
  }
});

Add it where you create the router instance. Done.

Verifying the setup actually works

Installing the code is half the job. The other half is confirming Hotjar receives the events. Three checks I run on every install:

1. Browser console check. Open DevTools, navigate through your app, and watch the Network tab. Filter for hotjar. You should see new requests fire on each route change. If you see one request on initial load and nothing after, your stateChange call isn't running.

2. Hotjar recordings. Trigger a session yourself. Click through a few routes. Wait for the recording to process (usually under 30 minutes). Open it and check that page URLs in the timeline update as you navigated. If every event is tagged with your homepage URL, the tracking is still broken.

3. Heatmap URL filter. Go to Heatmaps and try to create one for a non-root URL. If Hotjar shows zero data for routes you definitely visited, stateChange isn't firing.

Common gotchas

A few things bite people repeatedly:

Hash routing. If your app uses hash-based routing (/#/pricing), Hotjar may or may not pick up the hash depending on your config. Always pass the full path explicitly via stateChange instead of relying on auto-detection.

Query parameters and PII. If your URLs contain emails, user IDs, or other personal data in query strings, strip them before passing to stateChange. Hotjar has data suppression rules but cleaning the URL is safer.

Modal "page views." Some teams call stateChange when modals open. This pollutes your data. Modals are not pages. Use Hotjar events for those instead.

Race conditions. If your route change fires before the Hotjar script loads, the call silently fails. The if (window.hj) check prevents errors but you'll lose the first page view. To fix, queue the call:

window.hj = window.hj || function(){ (window.hj.q = window.hj.q || []).push(arguments) };
window.hj('stateChange', '/route');

This works because Hotjar processes the queue when it finishes loading.

When Hotjar still isn't the right fit

Even with perfect SPA setup, Hotjar has limits. Long sessions can get truncated. High-traffic sites burn through session quotas fast. If you have a complex SPA with deep funnels, you may hit walls.

If you're running into those, it's worth checking Hotjar alternatives built for SPAs and product analytics. FullStory and Smartlook handle SPA routing more gracefully out of the box. For pure landing page analysis without app complexity, the comparison in Crazy Egg vs Hotjar is also useful.

For pricing changes that may affect your decision, see what changed with Hotjar pricing in 2026.

Quick checklist before you ship

Run through this list before assuming your install is correct:

  • Hotjar base snippet installed site-wide, in the <head>
  • stateChange calls on every route change in your router config
  • Verified new Hotjar network requests fire on navigation
  • Test recording shows correct URLs per route
  • Sensitive query parameters stripped before tracking
  • Modal opens and other UI state changes do NOT trigger stateChange

Once those are green, your heatmaps and recordings reflect real user behavior per route instead of a confused blob.


If you want a faster way to spot landing page issues without wrestling with SPA configs, give PagePulse a try. We analyze your live page and surface conversion problems directly, no tracking script, no waiting for sessions to accumulate, no SPA setup gotchas. Paste a URL and get specific fixes in minutes.