---
title: "How to add heatmaps to a React app"
description: "Turn on session replay in a Vite and React Router app so Amplitude heatmaps can map clicks and scrolls per route, then create your first heatmap."
canonical: "https://amplitude.com/en-us/build/react-heatmaps"
---

# How to add heatmaps to a React app

Turn on session replay in a Vite and React Router app so Amplitude heatmaps can map clicks and scrolls per route, then create your first heatmap.

Source: https://amplitude.com/en-us/build/react-heatmaps

---

# How to add heatmaps to a React app

Turn on session replay in a Vite and React Router app, switch on heatmap capture for the project, give each tab its own route so heatmaps don't merge them, and create a click map for a dynamic route. About 15 minutes of setup, then heatmaps fill in as traffic arrives.

- React 18+
- Vite
- React Router
- Unified SDK 1.x
- 15 min

Chris Tam · Last updated 7 October 2026

React makes it easy to pack a whole settings screen into one component. [Heatmaps](https://amplitude.com/heatmaps) show where people click and how far they scroll on each page, so you can see which parts of that screen get used and which get ignored.

In this tutorial, you'll turn on the session replay recording heatmaps are built from, put each tab on its own route, and create your first heatmap. It builds on the [React session replay page](/build/react-session-replay).

## What you'll build

A Vite and React Router app that records session replay on every route, sends `[Amplitude] Page Viewed` with the same `device_id` as the replay, and gives the Profile and Billing tabs of `/settings` their own paths, plus a click map in Amplitude for every `/projects/:projectId` page. By the end you'll see a replay of your test session and a heatmap set up to fill in once traffic crosses the data threshold.

## Prerequisites

- A Vite project with React 18 or later, TypeScript, and React Router (`react-router-dom`) using `BrowserRouter`, so each route has its own path.
- 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; turning on heatmap capture for a project takes an Admin or Manager role.

New to Amplitude? [Sign up for free](https://app.amplitude.com/signup) to get your project API key, then [compare plans](https://amplitude.com/pricing) to add heatmaps.

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

## 1. Install the SDK

[The Quickstart](https://amplitude.com/docs/get-started/amplitude-quickstart) recommends `@amplitude/unified` for browser apps installed from npm. It bundles analytics, session replay, Feature Experiment, and guides and surveys behind one `initAll` call, and Amplitude keeps the `device_id` on replay and analytics events aligned for you, which heatmap segment and cohort filters depend on. If you followed the [React session replay page](/build/react-session-replay), it's already installed; skip to step 3.

Terminal

```sh
npm install @amplitude/unified
```

Already on `@amplitude/analytics-browser` to keep bundle size down? Add the [Session Replay Browser SDK Plugin](https://amplitude.com/docs/sdks/session-replay/session-replay-plugin) to your existing init instead of installing a second SDK. It takes the same options as the `sessionReplay` block in step 2.

Heatmaps need Session Replay Browser SDK Plugin 1.7.0 or later. The Verify section shows how to read the version your page actually runs.

Vite only exposes env vars prefixed with `VITE_` 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 anyway.

.env.local

```sh
VITE_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` above `0` turns recording on, and recording is what heatmaps are built from. Initialize once, at module scope:

src/lib/amplitude.ts

```typescript
import * as amplitude from '@amplitude/unified';

const apiKey = import.meta.env.VITE_AMPLITUDE_API_KEY;
const isProduction = import.meta.env.PROD;

if (apiKey) {
  amplitude.initAll(apiKey, {
    analytics: {
      autocapture: {
        attribution: true,
        // [Amplitude] Page Viewed on every route change.
        pageViews: true,
        sessions: true,
        // Adds page URL properties to events you track yourself.
        pageUrlEnrichment: true,
        formInteractions: true,
        fileDownloads: true,
      },
    sessionReplay: {
      // Every session in `vite dev`, 1% in a production build.
      sampleRate: isProduction ? 0.01 : 1,
      // Ignores the focus check so replays record with DevTools open. Setup only.
      debugMode: !isProduction,
    },
  });
} else {
  console.warn('VITE_AMPLITUDE_API_KEY is not set; Amplitude is off.');
}

export { amplitude };
```

Import it first in your entry file, above everything else:

src/main.tsx

```typescript
import './lib/amplitude';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { App } from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);
```

**Why.** Initializing more than once can leave replay and analytics events with different device or session IDs. Heatmaps still draw an unsegmented map in that case, but segment and cohort filters come back empty, because Amplitude can't join the clicks to your users. A module body runs once per page load, and Strict Mode doesn't run it twice the way it runs effects, so never move `initAll` into a component or a `useEffect`.

You don't need `elementInteractions` for heatmaps. The heatmap prerequisites list session replay, not click autocapture; the clicks on the map come from the replay.

Be deliberate about the sample rate. Every heatmap reflects only the sessions you captured, so at `0.01` a map is built from 1% of traffic and takes longer to cross the data threshold. Amplitude recommends starting low and raising the rate over a few days; capture stops for the month once your org's replay quota runs out. Session Replay Settings in Amplitude can also set the project's rate remotely, so check there when the rate you see doesn't match your code. For masking, read the [React session replay page](/build/react-session-replay) before you raise the rate in production: heatmap backgrounds are replay snapshots, so they show only what your masking lets through.

Unified also initializes guides and surveys by default. If you won't use them, add `engagement: { skip: true }` to the `initAll` options, as the [Unified SDK docs](https://amplitude.com/docs/sdks/analytics/browser/browser-unified-sdk) describe.

## 3. Turn on heatmap capture in Amplitude

The SDK records replays, but the project decides whether heatmap data gets collected. In Amplitude, go to Settings > Organizational Settings > Session Replay Settings. The project list shows a session capture toggle and a heatmap capture toggle for each project. Turn both on for this project.

Select the project to open its Sampling control. A rate set there applies to the project without a deploy, which is the easier place to raise capture once heatmaps need more data.

## 4. Give each view its own URL

Heatmaps compute data per URL string, not per view. A tab bar built with `useState` never changes the URL, so the Profile and Billing tabs of `/settings` end up in one heatmap, drawn over whichever tab the background happened to catch. React Router fixes this with nested routes: each tab becomes a child route, and the tab bar lives in a layout.

src/pages/settings/SettingsLayout.tsx

```typescript
import { NavLink, Outlet } from 'react-router-dom';

export function SettingsLayout() {
  return (
    <section>
      <h1>Settings</h1>
      <nav>
        <NavLink to="profile">Profile</NavLink>
        <NavLink to="billing">Billing</NavLink>
      </nav>
      {/* The active tab renders here. */}
      <Outlet />
    </section>
  );
}
```

src/pages/settings/ProfileTab.tsx

```typescript
export function ProfileTab() {
  return <h2>Profile</h2>;
}
```

src/pages/settings/BillingTab.tsx

```typescript
export function BillingTab() {
  return <h2>Billing</h2>;
}
```

src/App.tsx

```typescript
import { Navigate, Route, Routes } from 'react-router-dom';
import { Home } from './pages/Home';
import { Project } from './pages/Project';
import { SettingsLayout } from './pages/settings/SettingsLayout';
import { ProfileTab } from './pages/settings/ProfileTab';
import { BillingTab } from './pages/settings/BillingTab';

export function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/projects/:projectId" element={<Project />} />
      <Route path="/settings" element={<SettingsLayout />}>
        {/* /settings on its own redirects to the first tab. */}
        <Route index element={<Navigate to="profile" replace />} />
        <Route path="profile" element={<ProfileTab />} />
        <Route path="billing" element={<BillingTab />} />
      </Route>
    </Routes>
  );
}
```

Keep your existing `Home` and `Project` components. If your `App.tsx` already mounts `IdentifyUser` from the React product analytics page, keep it above `<Routes>`.

Now `/settings/profile` and `/settings/billing` get separate heatmaps, and each tab switch also sends its own `[Amplitude] Page Viewed`. The `replace` on the index redirect stops `/settings` itself from sitting in the browser history as a separate, empty page.

**Why.** Put the tab in the path, not in a query string such as `?tab=billing`. Heatmaps need the `[Amplitude] Page URL` property to match URLs, and that property is the page URL with query parameters removed, so `/settings?tab=profile` and `/settings?tab=billing` can land in the same heatmap. A path segment is unambiguous. Modals and drawers that don't change the URL also merge into the page underneath; if one matters, give it a child route the same way.

## 5. Create your first heatmap

Heatmaps appear after Amplitude collects enough replay click data across your site: at least 10 separate pages, with about 500 tracked replay clicks on each page. These thresholds are approximate. Create the heatmap now and it fills in once 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 map or scroll map at any time.
3. Select the URL. `/projects/:projectId` gives every project its own URL, so use **Pattern match** with `https://your-domain.com/projects/*` to combine them into one map. Use **Exact match** for a single page, such as `https://your-domain.com/settings/billing`, and **Starts with** for a section, such as `https://your-domain.com/settings`.
4. Optionally pick a segment of users by user property or cohort.
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 layout that collapses its nav into a menu below a CSS breakpoint looks different in each bucket, and the background and the click data must fall in the same one.
6. Choose a background. Each heatmap generates eight, taken from the page states with the most actions. Skip any that caught an open modal or a loading spinner.

Once your data meets the thresholds, Amplitude also creates a few heatmaps automatically for your highest-traffic pages. You can delete them; they aren't recreated.

If your routes carry personal data in the path, such as an email address in `/users/:email`, the [URL filter rules](https://amplitude.com/docs/session-replay/configure-ugc-filter-rules) replace those URLs before session replay and heatmaps record them.

**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](https://amplitude.com/docs/amplitude-ai/amplitude-mcp).

## Verify it worked

A development session can't reach the heatmap data threshold, so verification stops at confirming that replays arrive from a new enough SDK and that each tab has its own URL. Run `npm run dev`, open `http://localhost:5173` in a fresh window, click between `/settings/profile`, `/settings/billing`, and a project page, then check in this order:

1. **Session Replay debug panel.** In the [Instrumentation Explorer Chrome extension](https://chrome.google.com/webstore/detail/amplitude-event-explorer/acehfjhnmhbmgkedjmjlobpgdicnhkbp), turn on the Session Replay debug panel. It shows the current replay ID, the session replay SDK and plugin versions, and the 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`. You should see event uploads to `api2.amplitude.com` returning `200`, a request to `sr-client-cfg.amplitude.com`, and replay uploads to `api-sr.amplitude.com`.
3. **Ingestion Debugger.** In Amplitude, click Data, select Sources, then open the Ingestion Debugger tab. The successful requests chart should show your uploads. It shows counts, not individual events.
4. **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 one `[Amplitude] Page Viewed` per route you visited, and the `[Amplitude] Page URL` on the two settings views ends in `/settings/profile` and `/settings/billing`. A **Play Session** button appears for the session once the replay is processed, generally about five minutes after the session begins.
5. **Heatmaps.** Open the heatmap from step 5. In a development project it shows "No heatmap available" until the thresholds are met. After you deploy, 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: Amplitude is still processing new replay data (a few minutes after install), the URL filter doesn't match URLs in your events, the deployed site runs a session replay plugin older than 1.7.0, or traffic hasn't reached about 500 replay clicks on each of 10 pages. If `[Amplitude] Page URL` was hidden, restore it in Data > Properties.

### Every tab on a screen shows up in one heatmap

The tabs share a URL. Move each one to a child route as in step 4. A tab kept in a query string can merge too, because `[Amplitude] Page URL` drops query parameters; move it into the path.

### No replays at all, even with a sample rate of 1

Check the project's rate and capture toggles in Session Replay Settings (step 3). Then open the console: if you added a `Content-Security-Policy`, a blocked request appears there, and you need to add `https://api-sr.amplitude.com` and `https://sr-client-cfg.amplitude.com` to `connect-src`. If remote configuration fails to load, session replay captures nothing, on purpose. An ad blocker also stops capture; test in a clean profile.

### A 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 carry different `device_id` values, so Amplitude can't join clicks to users. Look for a second `initAll` or `init` call, for example inside a component or a `useEffect`, or a standalone session replay SDK running beside your analytics. Keep the single init in `src/lib/amplitude.ts`.

### "Heatmaps isn't available for your organization"

Your organization predates Amplitude's current packaging and needs manual enablement. Contact [Amplitude Support](https://gethelp.amplitude.com).

## Full code reference

Every file from this tutorial in one block, the version to hand to an agent. 8 files.

8 files, 97 lines

```typescript
// -- .env.local --
VITE_AMPLITUDE_API_KEY=your-project-api-key

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

const apiKey = import.meta.env.VITE_AMPLITUDE_API_KEY;
const isProduction = import.meta.env.PROD;

if (apiKey) {
  amplitude.initAll(apiKey, {
    analytics: {
      autocapture: {
        attribution: true,
        pageViews: true,
        sessions: true,
        pageUrlEnrichment: true,
        formInteractions: true,
        fileDownloads: true,
      },
    sessionReplay: {
      sampleRate: isProduction ? 0.01 : 1,
      debugMode: !isProduction,
    },
  });
} else {
  console.warn('VITE_AMPLITUDE_API_KEY is not set; Amplitude is off.');
}

export { amplitude };

// -- src/main.tsx --
import './lib/amplitude';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { App } from './App';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
);

// -- src/App.tsx --
import { Navigate, Route, Routes } from 'react-router-dom';
import { Home } from './pages/Home';
import { Project } from './pages/Project';
import { SettingsLayout } from './pages/settings/SettingsLayout';
import { ProfileTab } from './pages/settings/ProfileTab';
import { BillingTab } from './pages/settings/BillingTab';

export function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/projects/:projectId" element={<Project />} />
      <Route path="/settings" element={<SettingsLayout />}>
        <Route index element={<Navigate to="profile" replace />} />
        <Route path="profile" element={<ProfileTab />} />
        <Route path="billing" element={<BillingTab />} />
      </Route>
    </Routes>
  );
}

// -- src/pages/settings/SettingsLayout.tsx --
import { NavLink, Outlet } from 'react-router-dom';

export function SettingsLayout() {
  return (
    <section>
      <h1>Settings</h1>
      <nav>
        <NavLink to="profile">Profile</NavLink>
        <NavLink to="billing">Billing</NavLink>
      </nav>
      <Outlet />
    </section>
  );
}

// -- src/pages/settings/ProfileTab.tsx --
export function ProfileTab() {
  return <h2>Profile</h2>;
}

// -- src/pages/settings/BillingTab.tsx --
export function BillingTab() {
  return <h2>Billing</h2>;
}

// -- src/pages/Home.tsx, src/pages/Project.tsx --
// Your existing pages. Nothing in them changes.
```

## 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

```text
Set up Amplitude heatmaps in this Vite + React Router app by following
https://amplitude.com/build/react-heatmaps exactly. If the app already
uses @amplitude/analytics-browser, add
@amplitude/plugin-session-replay-browser (1.7.0 or later) to the existing
init instead of installing a second SDK. Otherwise use
@amplitude/unified. Call initAll once at module scope in
src/lib/amplitude.ts, reading VITE_AMPLITUDE_API_KEY from
import.meta.env, and import that module first in src/main.tsx. Never
initialize inside a component or useEffect. Set sessionReplay.sampleRate
to 1 in development and 0.01 when import.meta.env.PROD is true, with
debugMode only in development, and keep pageViews and pageUrlEnrichment
on. Find every screen that switches tabs, steps, or panels with local
state or a query parameter under one URL and list them for me, with a
proposal to move each into nested child routes; don't restructure routes
without asking. Tell me that session replay now records sessions and how
to lower or disable the rate. Don't hardcode the key. Report every file
you changed.
```

## What this unlocks

- [Session replay](https://amplitude.com/session-replay), already on from step 2, lets you select an area of a click map and watch the sessions behind those clicks.
- [Product analytics](https://amplitude.com/amplitude-analytics) can chart `[Amplitude] Page Viewed` for `/settings/billing` against your upgrade event to see whether the tab you just mapped converts.
- [Feature Experiment](https://amplitude.com/amplitude-experiment), bundled in `@amplitude/unified`, can test a new billing layout, and a heatmap per variant shows how the clicks moved.
- [Guides and surveys](https://amplitude.com/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

[Web frameworksAdd session replay to a React appMask sensitive components and whole routes before you raise the sample rate.](/build/react-session-replay)[Web frameworksAdd web analytics to a React appSee which channels send the visitors whose clicks you're mapping.](/build/react-web-analytics)[Web frameworksAdd product analytics to a React appIdentify signed-in users, so heatmap segments can filter by plan or role.](/build/react-product-analytics)

Reference docs

[Heatmaps →](https://amplitude.com/docs/heatmaps)[Create a heatmap →](https://amplitude.com/docs/heatmaps/create-a-heatmap)[Troubleshoot heatmaps →](https://amplitude.com/docs/heatmaps/troubleshooting)[Session Replay Browser SDK Plugin →](https://amplitude.com/docs/sdks/session-replay/session-replay-plugin)[Manage Session Replay settings →](https://amplitude.com/docs/session-replay/session-replay-settings)[Amplitude MCP server →](https://amplitude.com/docs/amplitude-ai/amplitude-mcp)

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

For agents

[View raw .md](/build/react-heatmaps.md)[llms.txt](/llms.txt)

## 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.

[Create a free account](https://app.amplitude.com/signup)[Browse all Build tutorials](https://amplitude.com/build)
