Loading tutorials…
Loading tutorials…
Out of the box, Hotjar tracks page views, clicks, and scroll. Anything beyond that — button clicks on a SPA, app-level milestones, custom funnel steps — needs custom events. This is the install path that doesn't break.
Who this is forTeams running single-page apps (React, Vue, Next.js) or custom-built sites where Hotjar's auto-capture misses key actions. Or teams who need to filter recordings/funnels by custom behavior (e.g., 'users who clicked the upgrade CTA').
What you'll need
Step 1
Hotjar auto-captures clicks, page views, and scroll. Custom events are for actions Hotjar can't auto-detect: SPA route changes, button clicks on dynamic UI, app-level milestones.
List the actions you want to filter recordings or build funnels around. Examples: "added_to_cart", "started_trial", "viewed_pricing_tier", "submitted_lead_form", "applied_coupon".
For static-URL sites, you usually don't need custom events — Hotjar's URL-based targeting covers most cases. Use custom events when the action doesn't change the URL.
For SPAs (React/Vue/Next.js): you almost always need at least route-change events if Hotjar's automatic SPA detection misses your routing pattern. Test first — Hotjar auto-detects pushState/replaceState changes by default.
Cap your initial event list at 5-10. More than that and you'll struggle to maintain consistent naming. Start small and expand.
Event naming convention: snake_case, verb_noun format. "added_to_cart" not "AddToCart" or "user_added_item". Hotjar treats event names as case-sensitive strings — inconsistent casing creates duplicate events.
Step 2
Call hj("event", "event_name") from your site code at the moment the action happens.
Hotjar exposes a global hj() function once the snippet loads. The event API is: hj("event", "event_name").
Example for a cart add button: in your onClick handler, call hj('event', 'added_to_cart') right before (or after) your existing cart logic.
Example for a React component: useEffect(() => { if (window.hj) window.hj('event', 'viewed_pricing_tier'); }, []) on the pricing-tier component.
Guard against hj not being loaded: always check typeof window !== 'undefined' && window.hj before calling. Especially important for SSR (Next.js) where the snippet won't have loaded server-side.
Place the event call at the moment the action *completes*, not the moment the button is clicked. Example: fire "added_to_cart" after the API call succeeds, not on click — otherwise you count failed adds as successful.
Step 3
If you already run GTM, fire Hotjar events from there. Cleaner separation of marketing logic from app code.
In GTM, create a new Custom HTML tag.
Tag content: <script>if (typeof window !== 'undefined' && window.hj) { window.hj('event', 'added_to_cart'); }</script>
Set the trigger based on what already exists in GTM: Click → All Elements where Click Classes contains "add-to-cart-button". Or Custom Event triggered by a dataLayer.push from your app.
For dataLayer-driven events: have your dev team push dataLayer.push({event: "cart_add"}) from the app, then GTM listens for that event and fires the Hotjar tag.
Pro: GTM keeps event logic out of app code, making it easier for marketers to add/remove events without dev cycles. Con: requires GTM expertise; if you don't already use it, adopting it just for Hotjar events is overkill.
Step 4
Visit your site, trigger the event, then check Hotjar → Events to confirm it's been recorded.
Open Hotjar → Events (Business+ plans show this in the sidebar; lower plans see events under Filters in Recordings).
In a clean incognito browser, trigger the event on your site (click the button, complete the action).
Wait 1-2 minutes. Refresh the Hotjar Events page. Your event name should appear in the list with a session count of 1.
If it doesn't appear after 5 minutes: open DevTools Console on the page where you triggered it. Type window.hj and confirm it's a function. Type window.hj('event', 'test_event_verify') and press Enter. If the test event shows up but your real event doesn't, the original call isn't executing — debug with breakpoints.
Once verified, recordings of sessions that triggered the event will show the event in the session timeline (right-hand panel of the recording player). You can also filter recordings by event from this point forward.
Step 5
For logged-in users, push their identity to Hotjar. This unlocks cross-session funnels and user-segment filtering.
If you have logged-in users (SaaS, ecommerce account, etc.), call hj('identify', userId, {attribute_name: value, ...}) once per session after login.
Example: hj('identify', user.id, { plan: 'premium', signup_date: user.createdAt, ltv: user.lifetimeValue })
userId should be a stable identifier (your internal user ID, not email — emails are PII and shouldn't flow to Hotjar). Hashed/encrypted IDs are fine.
Attributes you push become filterable in Recordings, Funnels, and Surveys. Example uses: filter recordings to "only sessions from premium plan users" or build a funnel "trial → paid for users from organic only."
GDPR caveat: Hotjar treats user IDs as PII when paired with attributes. Make sure your Privacy Policy discloses Hotjar use and your consent banner gates the identify() call until consent is granted.
Step 6
On Business plan, you can build funnels with event-based steps instead of URL steps. Required for SPAs without URL changes.
Open Funnels → New funnel. For each step, instead of "URL matches" pick "Event triggered".
Pick the event name from the dropdown (Hotjar populates this from events that have fired at least once). If your event isn't in the dropdown, it hasn't fired yet — fire a test event first.
Example SPA signup funnel: visited_pricing (event) → clicked_get_started (event) → completed_signup_form (event) → verified_email (event) → dashboard_loaded (event). No URL changes needed.
You can mix URL steps and Event steps in one funnel. Useful for hybrid: URL step for the landing page, event step for the SPA conversion modal.
Same drop-off → recordings workflow as URL-based funnels (see Set Up Hotjar Funnels). Click any step's bar to see recordings of users who dropped at that step.
Step 7
Events go stale. New features add events; old features deprecate events. Maintain a single source of truth doc so the team uses consistent names.
Maintain an Events Schema doc (Notion, Linear, Google Doc) listing every Hotjar event you fire, where it fires, and what it represents.
Schema row format: event_name | trigger | page | added_date | owner | retire_date. Example: added_to_cart | onClick on .add-to-cart | /products/* | 2026-05-01 | Sarah | null.
Quarterly: prune events that no longer fire or that the team doesn't reference. Each unused event clutters the dropdown and tempts mis-use.
When deprecating: keep the old event firing for 30 days alongside the new one (e.g., add_to_cart AND added_to_cart simultaneously), then remove the old one. Avoids breaking saved filters and funnels mid-migration.
Whenever your dev team ships a UI change, ask: does this break any event triggers? CSS class changes are the #1 source of silent event-tracking breakage.
Common mistakes
Inconsistent event naming across team and pages
What goes wrong: Same action fires as "add_to_cart" on the product page, "AddToCart" in the cart page, and "added-to-cart" in mobile. Hotjar treats each as a separate event. Your funnel chart shows 5 events for what is one user action; the data is unreadable. Wasted ~$200-600/mo of Business plan capacity on cluttered events.
How to avoid: Adopt a naming convention (snake_case, verb_noun) and document it. Audit existing events monthly. Rename or deprecate inconsistent ones via the migration pattern (run both for 30 days, then remove the old).
Firing events in a useEffect without dependency array
What goes wrong: Event fires on every React re-render — sometimes 30-100 times per page view. Your event count in Hotjar shows 4,000 "viewed_pricing" events for 80 actual page views. Funnels and filters are useless. Decisions get made on garbage data. Typically costs $3,000-8,000 in misdirected CRO experiments before the team realizes the events are over-firing.
How to avoid: Pin events with empty dependency array [] in useEffect for once-per-mount. Or fire from explicit event handlers (onClick, onSubmit) instead of effects.
Passing PII as user attributes
What goes wrong: hj('identify', userId, { email: 'user@example.com' }) leaks PII into Hotjar in violation of consent policies. Under GDPR, this is a data-processing violation. Fines start at €10K. Even US-only operators get burned when EU traffic shows up.
How to avoid: Pass internal user IDs only. For email/name follow-up, store server-side and join with Hotjar via user ID after the fact. Hotjar's docs explicitly warn against PII in identify().
Never verifying events in production
What goes wrong: Events were tested in staging and worked. In production, a CSP header blocks the Hotjar script on a subset of pages, or a feature flag hides the trigger element from some users. Events fire for 60% of users but not 40%. Funnel data is silently broken — and the broken funnel can mislead $5,000-20,000 in ad-spend optimization decisions before the team notices.
How to avoid: After every deploy that touches event triggers, run a verification pass: open Hotjar Events, trigger each event in production incognito, confirm session count increments. 5 minutes; saves weeks of bad data.
Treating Hotjar events as a replacement for GA4 events
What goes wrong: Team configures rich event tracking in Hotjar, skips GA4 events, then realizes Google Ads Smart Bidding can't use Hotjar events. Conversion data isn't flowing to ad platforms. Smart Bidding optimizes blindly. ~$2K-10K/mo of wasted spend depending on account size.
How to avoid: Fire critical conversion events to BOTH Hotjar and GA4. GA4 events feed ad platforms; Hotjar events filter recordings + surveys. Use the same event names in both for sanity.
No event schema documentation
What goes wrong: Six months in, no one remembers which events fire from where. New team members rename events accidentally. A re-deploy breaks 3 events silently. Funnels show drop-off everywhere because the events stopped firing. Took weeks to diagnose — usually $4,000-10,000 in lost dev time + the CRO insights you couldn't draw because your data was broken during the outage.
How to avoid: Maintain an Events Schema doc with event name, trigger, page, owner, and last-verified date. Review quarterly. Treat it as code — single source of truth.
Recap
Done — what's next
How to set up Hotjar Funnels and find drop-off
Read the next tutorial
Hand it off
Event tracking is where DIY Hotjar usually stalls — it requires JavaScript or GTM expertise plus discipline around naming and verification. A specialist installs the standard event library in one afternoon ($80-200 one-time) and maintains it quarterly as part of an ongoing CRO engagement at $14-16/hr.
See specialist rates
Both track custom user actions, but for different purposes. GA4 Events feed Google Ads Smart Bidding and standard analytics reports. Hotjar Events filter recordings/funnels and trigger surveys. Most professional stacks fire critical conversion events to both, using identical event names.
Yes. In the survey targeting tab, pick "Display on JS Trigger" and reference the event name. Useful for things like "fired the rage_click event 2+ times in this session → show a survey asking what's broken."
No — events themselves don't consume quota. Sessions consume quota. Events are metadata attached to sessions. Fire as many events as you need; just keep them organized.
Limited. Hotjar is primarily a client-side tool — events need to fire from the user's browser to associate with a session. Server-side events via the Hotjar API are possible but rarely used; they require manually correlating server events with user sessions.
Events follow your plan's session retention (365 days on most paid plans). Once a session is deleted, the events attached to it are deleted. Export events you reference long-term via the Hotjar API or CSV exports.
Not natively. The standard pattern is: dataLayer.push({event: "added_to_cart"}) from your app → GTM fires the GA4 event AND the Hotjar event in parallel. The dataLayer is the single source of truth; both tools receive the same data.
Hotjar
GA4 tells you 60% of users abandoned at checkout step 2. Hotjar Funnels tells you which 60% — and lets you click straight into 5 recordings of the people who dropped off. That's the workflow.
Hotjar
Hotjar's recordings are the most powerful feature in the tool — and the most-wasted. The difference is filter discipline. This is the setup that turns 1,000 recordings/week into 5 useful insights, not 1,000 hours of "someday I'll watch these."
Hotjar
GA4 tells you what happened across thousands of sessions. Hotjar shows you why for 5 of them. Connecting the two means you can click from a GA4 anomaly straight into the 5 Hotjar recordings that explain it. This is the workflow.
Hotjar
DIY Hotjar is a great idea — until it isn't. This is the honest framework: when the cost of unwatched recordings and unanalyzed surveys exceeds the cost of hiring help, and how to tell which side you're on.