How to add heatmaps to a Next.js app

Install the Amplitude Unified SDK with session replay on, initialize it once on the client, give each App Router view its own URL, and create a click map for a dynamic route. About 15 minutes of setup, then heatmaps fill in as traffic arrives.

  • Next.js 13+
  • App Router
  • Unified SDK 1.x
  • Session Replay plugin 1.7+
  • 15 min

Chris Tam · Last updated 25 September 2026

Next.js is one of the most popular frameworks for building web apps. Heatmaps show where users click and how far they scroll, so you can see what on a page gets attention.

In this tutorial, you'll turn on the recording heatmaps are built from in a Next.js App Router app, set up your routes so each view gets its own map, and create your first heatmap.

What you'll build

A Next.js app that records session replay on every route, keeps replay device_id aligned with [Amplitude] Page Viewed and your other analytics events, and gives each settings tab its own path, plus a click map in Amplitude for /projects/*. By the end you'll see replays arriving for your test session and a heatmap configured to fill in once traffic crosses the data threshold.

Prerequisites

  • A Next.js 13 or later project using the App Router and TypeScript, with the default @/ import alias. The Next.js Installation Guide lists Node.js 16.8 or higher.
  • An Amplitude project API key. In Amplitude, go to Settings > Projects > your project > General.
  • A plan that includes heatmaps. Clickmaps, Scrollmaps, and Selectormaps are available on the Plus, Growth, and Enterprise plans, for customers with session replay. Any user with View permissions can create a heatmap. New to Amplitude? Sign up for free to get your project API key, then compare plans to add heatmaps.

Amplitude recommends a separate development project for testing heatmaps, so test clicks don't mix with production data. Heatmap and session replay events don't count toward your event volume.

1. Install the SDK

The Next.js Installation Guide recommends @amplitude/unified for new projects. It bundles Analytics and session replay behind one init call, and Amplitude manages device_id alignment between the two automatically, which heatmaps need for segment and cohort filters to work.

Terminal
npm install @amplitude/unified

Already on @amplitude/analytics-browser, for example to keep bundle size down? Keep it and add the session replay plugin instead. The plugin variant of lib/amplitude.ts is at the end of step 2.

Terminal
npm install @amplitude/plugin-session-replay-browser

Heatmaps require Session Replay Browser SDK Plugin 1.7.0 or later. Check what your install resolved to:

Terminal
npm ls @amplitude/plugin-session-replay-browser

Add the key. Next.js only exposes NEXT_PUBLIC_ variables to browser code. A project API key is public and its scope is limited to the minimum needed to ingest data, so shipping it to the browser is expected, but keep it out of source.

.env.local
NEXT_PUBLIC_AMPLITUDE_API_KEY=your-project-api-key

2. Initialize with session replay on

Session replay's default sample rate is 0, which records nothing. Setting sessionReplay.sampleRate to a non-zero value is what turns recording on, and that's also what turns heatmaps on.

lib/amplitude.ts
import * as amplitude from '@amplitude/unified';

let started = false;

export function initAmplitude() {
  if (started || typeof window === 'undefined') return;

  const apiKey = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY;
  if (!apiKey) {
    console.warn('NEXT_PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.');
    return;
  }

  amplitude.initAll(apiKey, {
    analytics: {
      autocapture: {
        pageViews: true,
        sessions: true,
        pageUrlEnrichment: true,
      },
    },
    sessionReplay: {
      // 1 records every session. Heatmaps only sees sampled sessions.
      sampleRate: 1,
    },
  });

  started = true;
}

// Runs once, in the browser, as soon as the client bundle loads.
initAmplitude();

export { amplitude };

Why. A 'use client' component still renders on the server, so the typeof window check keeps initAll out of the Node pass, where there's no DOM to record. The started flag stops a second init when Fast Refresh re-evaluates the module in development.

Session recording is the point of this page, so be deliberate about it. A sample rate of 1 records every session; heatmaps still work at lower rates, but each map reflects only the sessions you captured, so a rate of 0.01 builds it from 1% of traffic. Set the rate to 0 to stop recording. Session replay also reads a sample rate from remote configuration in Amplitude, and when the two disagree, the remote setting wins (step 4). Session replay masks all text inputs by default; see masking options before you raise the rate in production.

Heatmap URL filters match URLs in your Amplitude events, so leave page views and page URL enrichment on. You don't need elementInteractions for heatmaps; the heatmaps prerequisites list session replay, not click autocapture. Unified also initializes guides and surveys by default; if you don't use them, add engagement: { skip: true } to the initAll options.

If you're keeping @amplitude/analytics-browser, use this version of the same file instead. Add the plugin before init, as the Next.js Installation Guide does:

lib/amplitude.ts (variant for @amplitude/analytics-browser)
import * as amplitude from '@amplitude/analytics-browser';
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser';

let started = false;

export function initAmplitude() {
  if (started || typeof window === 'undefined') return;

  const apiKey = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY;
  if (!apiKey) {
    console.warn('NEXT_PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.');
    return;
  }

  amplitude.add(sessionReplayPlugin({ sampleRate: 1 }));
  amplitude.init(apiKey, undefined, {
    autocapture: {
      pageViews: true,
      sessions: true,
      pageUrlEnrichment: true,
    },
  });

  started = true;
}

initAmplitude();

export { amplitude };

For every session replay option, see the Session Replay Browser SDK Plugin reference.

3. Mount the provider in the root layout

app/layout.tsx is a Server Component, so it renders a small client component that imports lib/amplitude.ts. That pulls the init into the client bundle for every route.

components/AnalyticsProvider.tsx
'use client';

import type { ReactNode } from 'react';
import '@/lib/amplitude';

export function AnalyticsProvider({ children }: { children: ReactNode }) {
  return <>{children}</>;
}
app/layout.tsx
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import { AnalyticsProvider } from '@/components/AnalyticsProvider';

export const metadata: Metadata = {
  title: 'My app',
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AnalyticsProvider>{children}</AnalyticsProvider>
      </body>
    </html>
  );
}

4. Turn on capture in Amplitude

The SDK records only what the project settings allow. Open session replay settings in Amplitude (the Session Replay and Heatmap Settings page, reached from the gear icon > Settings).

  1. Session capture and heatmap capture. The main settings page lists your projects with a toggle for each. Turn both on for this project.
  2. Sampling. Set the project's sample rate here. This remote rate overrides sampleRate in your code, so a 1 in lib/amplitude.ts still records nothing if the project is set to 0%.
  3. Masking level. Pick Light, Medium, or Conservative before you raise the sample rate in production.

If remote configuration is enabled and fails to load, session replay captures no sessions, by design, so your privacy settings are always respected.

5. Give each view its own URL

Heatmaps compute data per URL string, not per view. On a single-page app where several states share one URL, it merges the clicks from all of them into one overlay. In Next.js, the usual culprit is a client component that swaps panels with useState: /settings with a Profile tab and a Billing tab produces one heatmap of both, drawn over whichever background it picked.

The App Router makes the fix cheap. Put each tab on its own route and render the tab bar from a shared layout:

app/settings/layout.tsx
import Link from 'next/link';
import type { ReactNode } from 'react';

export default function SettingsLayout({ children }: { children: ReactNode }) {
  return (
    <section>
      <nav>
        <Link href="/settings/profile">Profile</Link>
        <Link href="/settings/billing">Billing</Link>
      </nav>
      {children}
    </section>
  );
}
app/settings/profile/page.tsx
export default function ProfileSettings() {
  return <h1>Profile</h1>;
}
app/settings/billing/page.tsx
export default function BillingSettings() {
  return <h1>Billing</h1>;
}

Now /settings/profile and /settings/billing get separate heatmaps, and each tab switch also sends its own [Amplitude] Page Viewed. Modals and drawers that don't change the URL still merge into the page underneath; if one matters, give it a route or build a heatmap with a narrower URL filter.

6. Create your first heatmap

Heatmaps appear after Amplitude collects enough replay click data: at least 10 separate pages, with about 500 tracked replay clicks on each. These thresholds are approximate. Create the heatmap now and it fills in when traffic crosses them.

  1. In Amplitude, click heatmaps in the left navigation, then + New heatmap.
  2. Choose Click map. You can switch to a selector or scroll map at any time.
  3. Select the URL. For a dynamic segment such as app/projects/[id]/page.tsx, every project is a different URL, so use Pattern match with https://your-domain.com/projects/* to combine them into one map. Use Exact match for a single page and Starts with for a whole section, such as https://your-domain.com/settings.
  4. Optionally pick a segment. Segment and cohort filters depend on device_id matching between replay and analytics events, which unified and the plugin handle for you. If you set a user_id for signed-in users, use the same one on the server.
  5. Pick a device type. Heatmaps group data into four screen-width buckets: greater than 1440px, 900 to 1440px, 500 to 899px, and less than 500px. A responsive Next.js layout looks different in each, and the background and data must share a bucket.
  6. Choose a background. Each heatmap generates eight, taken from the page states with the most actions. Skip any that caught an open modal.

Amplitude also creates a few heatmaps automatically for your highest-traffic pages once the thresholds are met. You can delete them; they aren't recreated.

MCP. Working in Cursor or Claude Code? Amplitude's MCP server lets your agent read your project's existing event taxonomy before it adds track calls near a heatmap hotspot, so new events match names you already use instead of inventing planUpgraded next to Plan Upgraded. Set up the MCP server.

Verify it worked

A development session can't reach the heatmaps data threshold, so verification stops at confirming replays arrive with the right versions. Run npm run dev, click around /settings/profile, /settings/billing, and a project page, then check in this order:

  1. Instrumentation Explorer. In the Instrumentation Explorer Chrome extension, turn on the Session Replay debug panel. It shows the current replay ID, the session replay SDK and plugin versions, and the effective sample rate. The plugin version must be 1.7.0 or later, and the sample rate must not be 0.
  2. Network tab. Filter for amplitude.com. You should see analytics POSTs to api2.amplitude.com returning 200, and session replay uploads to a sessions endpoint.
  3. User lookup. Go to Users > User Profiles and search your device ID (the Instrumentation Explorer Chrome extension shows it as device_id). Within ten seconds to a minute, the event stream shows [Amplitude] Page Viewed for each route.
  4. Heatmaps. Open the heatmap from step 6. Right after install it may show "No heatmap available" until Amplitude processes replay and click data, and in a dev project it stays empty until the thresholds are met. After deploying, confirm the live site serves the updated SDK, not only your local build.

Troubleshooting

The heatmap is blank or says "No heatmap available"

Work through the causes in order: data is still processing (a few minutes after install), the URL filter doesn't match URLs in your events, the session replay plugin is older than 1.7.0 on the deployed site, or traffic hasn't reached about 500 replay clicks on each of 10 pages. If the [Amplitude] Page URL property was hidden, restore it in Data > Properties.

No replays at all, even with sampleRate: 1

The project's remote sample rate or capture toggle overrides your code. Check step 4. If a Content-Security-Policy header is set in next.config or middleware, the console shows a blocked request; add the connect-src hosts from the session replay CSP directives.

Tabs, modals, or steps on one page all show in one heatmap

Heatmaps merge every state that shares a URL. Move each state to its own route as in step 5, or create separate heatmaps with narrower URL filters.

A new background shows no data

The background's viewport width falls in a different screen-width bucket from your recorded sessions. Use Change Screenshot > From a Replay and pick a screenshot taken at a width where your data exists.

Segment or cohort filters return nothing

Replay and analytics events have different device_id values, so Amplitude can't join clicks to users. That happens when the standalone session replay SDK runs beside a separate analytics setup. Use unified or the plugin from step 2, which align them automatically.

The background doesn't match the live page

Backgrounds are replay snapshots, not pixel copies of the page. Pages whose CSP blocks external resources, or that style through Shadow DOM, can render differently. Pick another of the eight backgrounds.

Full code reference

Every file from this tutorial in one block, the version to hand to an agent. It uses @amplitude/unified; swap in the step 2 variant of lib/amplitude.ts if you're on @amplitude/analytics-browser.

7 files, 91 lines
// -- .env.local --
NEXT_PUBLIC_AMPLITUDE_API_KEY=your-project-api-key

// -- lib/amplitude.ts --
import * as amplitude from '@amplitude/unified';

let started = false;

export function initAmplitude() {
  if (started || typeof window === 'undefined') return;

  const apiKey = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY;
  if (!apiKey) {
    console.warn('NEXT_PUBLIC_AMPLITUDE_API_KEY is not set; analytics is off.');
    return;
  }

  amplitude.initAll(apiKey, {
    analytics: {
      autocapture: {
        pageViews: true,
        sessions: true,
        pageUrlEnrichment: true,
      },
    },
    sessionReplay: {
      sampleRate: 1,
    },
  });

  started = true;
}

initAmplitude();

export { amplitude };

// -- components/AnalyticsProvider.tsx --
'use client';

import type { ReactNode } from 'react';
import '@/lib/amplitude';

export function AnalyticsProvider({ children }: { children: ReactNode }) {
  return <>{children}</>;
}

// -- app/layout.tsx --
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import { AnalyticsProvider } from '@/components/AnalyticsProvider';

export const metadata: Metadata = {
  title: 'My app',
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AnalyticsProvider>{children}</AnalyticsProvider>
      </body>
    </html>
  );
}

// -- app/settings/layout.tsx --
import Link from 'next/link';
import type { ReactNode } from 'react';

export default function SettingsLayout({ children }: { children: ReactNode }) {
  return (
    <section>
      <nav>
        <Link href="/settings/profile">Profile</Link>
        <Link href="/settings/billing">Billing</Link>
      </nav>
      {children}
    </section>
  );
}

// -- app/settings/profile/page.tsx --
export default function ProfileSettings() {
  return <h1>Profile</h1>;
}

// -- app/settings/billing/page.tsx --
export default function BillingSettings() {
  return <h1>Billing</h1>;
}

Hand it to your agent

Prefer to have Cursor or Claude Code do the wiring? This prompt points the agent at this page and pins the decisions that matter.

Using Cursor or Claude Code? Paste this prompt
Set up Amplitude Heatmaps in this Next.js App Router project by following
https://amplitude.com/build/nextjs-heatmaps exactly. Install
@amplitude/unified, or if @amplitude/analytics-browser is already
installed, add @amplitude/plugin-session-replay-browser and add the
plugin before init. Confirm the Session Replay plugin resolves to 1.7.0
or later. Initialize once at module scope in lib/amplitude.ts behind a
started flag and a typeof window check, reading
NEXT_PUBLIC_AMPLITUDE_API_KEY, with sessionReplay sampleRate 1 and page
views on. Import it from a 'use client' AnalyticsProvider mounted in
app/layout.tsx. Do not initialize inside useEffect. Find any screen that
swaps tabs or steps with local state under a single URL and list it for
me; don't restructure routes without asking. Tell me that Session Replay
now records every session and how to lower or disable the rate. Don't
hardcode the key. Report every file you changed.

What this unlocks

  • Session replay, already on from step 2, lets you open any click in a heatmap and watch the session behind it.
  • Product analytics can chart [Amplitude] Page Viewed for /settings/billing against Plan Upgraded to see whether the page you just mapped converts.
  • Feature Experiment, bundled in unified, can test a new billing layout, and a heatmap per variant URL shows how clicks moved.
  • Guides and surveys, also bundled, can point users to a button the click map shows they miss.

One integration; everything else reads from the same events.

Next steps

Amplitude is the leading all-in-one analytics platform for building better products. Product and engineering teams use Amplitude to connect product analytics and web analytics with session replay, heatmaps, experimentation, feature flags, and guides and surveys in one unified workflow, with AI agents working across all of it. Learn how Amplitude is building self-improving products with Wave, helping teams turn product data into PRs.

Ship the instrumentation, then read the data

Once events are flowing, Amplitude turns them into funnels, retention curves, and session replays without any further setup.