Events

How to track button clicks, signups and purchases as custom events

A practical guide to custom events: what to track, how to name events, and copy-paste examples for plain HTML, React and Next.js.

· 5 min read

Pageviews tell you where people went. Custom events tell you what they did: clicked “Get started”, finished signup, bought something, downloaded the PDF. Without them you can see traffic, but you can’t answer the question that matters: which pages and sources produce results?

This guide covers what’s worth tracking, how to name events so your reports stay readable, and copy-paste examples for plain HTML, React and Next.js. The code uses the Cool Analytics tracker, but the ideas apply to any tool.

What to track (and what not to)

A good event answers a business question. Start with a handful:

  • Primary conversion. Signup, purchase, demo booked, lead form sent. You need this one.
  • Key intent signals. Clicked a pricing plan, started checkout, opened the signup form.
  • Off-site goals. Clicked a phone number, an email link, an app store badge or an affiliate link.
  • Content goals. Downloaded a file, played a video, copied a code snippet.

Avoid tracking every click on everything. Fifty events nobody looks at make the five important ones harder to find. If you can’t say what decision an event would change, skip it.

One rule above all: never put personal data in events. No emails, names, phone numbers or user IDs from your database in event names or properties. Use categories instead (plan: "pro", not email: "jane@…").

Naming events so they stay useful

Pick a convention once and write it down. A simple one that works well:

  • lowercase with underscores or hyphens: signup, checkout_started, pricing_plan_clicked
  • object then action, past tense: video_played, file_downloaded
  • no variable data in the name: plan_clicked with a plan property, not plan_pro_clicked, plan_team_clicked…

The last rule matters most. Put variations in properties, and the event list stays short while the detail is still there.

The basic call

Once the tracker is on your site, it exposes a small API on window.hb:

// An event with just a name
hb.track("newsletter_joined");

// An event with properties
hb.track("plan_clicked", { plan: "pro", billing: "yearly" });

// A purchase: an "amount" property is treated as revenue
hb.track("purchase", { amount: 49, currency: "USD" });

A few details worth knowing:

  • Events are attached to the current page and to the visitor’s journey, so you can see which page someone was on and which source originally brought them.
  • An event whose properties include amount (or revenue, value or price) with a positive number is shown as revenue.
  • Event names that look like a signup (signup, sign_up, register, create_account) are recognised as signups.
  • Names are capped at 80 characters and properties at about 500 characters of JSON, which is plenty for a few labels.

The script loads with defer, so very early code may run before it exists. Use optional chaining to be safe: window.hb?.track("signup").

Plain HTML

For a button or link, an inline handler is the quickest option:

<a href="/signup" onclick="window.hb && hb.track('cta_clicked', { location: 'hero' })">
  Start free trial
</a>

For a form, track on submit:

<form id="contact">…</form>
<script>
  document.getElementById("contact").addEventListener("submit", function () {
    window.hb && hb.track("lead_sent", { form: "contact" });
  });
</script>

A tip for tracking many links without touching each one: add a data attribute and a single listener.

<a href="/whitepaper.pdf" data-event="file_downloaded" data-file="whitepaper">Download</a>

<script>
  document.addEventListener("click", function (e) {
    var el = e.target.closest("[data-event]");
    if (!el || !window.hb) return;
    var props = Object.assign({}, el.dataset);
    delete props.event;
    hb.track(el.dataset.event, props);
  });
</script>

React and Next.js

First, give TypeScript a type for the global so you get autocompletion and no errors:

// types/hb.d.ts
declare global {
  interface Window {
    hb?: { track: (name: string, props?: Record<string, unknown>) => void; ignore: () => void };
  }
}
export {};

Then a tiny helper you can import anywhere:

// lib/track.ts
export const track = (name: string, props?: Record<string, unknown>) => window.hb?.track(name, props);

Use it in any client component:

"use client";
import { track } from "@/lib/track";

export function PricingButton({ plan }: { plan: string }) {
  return (
    <button onClick={() => track("plan_clicked", { plan })}>
      Choose {plan}
    </button>
  );
}

Route changes in Next.js, React Router, Vue Router and SvelteKit are tracked as pageviews automatically, so you only need track for actions. The Next.js installation guide shows where to put the script itself.

Tracking signups and purchases reliably

Client-side events can be lost if the page navigates away immediately, if the visitor has an aggressive blocker, or if the success happens on a different domain (like a hosted checkout). Some practical approaches:

  • Track on the success page, not the button. Fire purchase on your “Thanks for your order” page, where you know the payment went through. Clicking “Pay” only shows intent.
  • Guard against double counting. If people can reload the thank-you page, store a flag in sessionStorage after the first event.
  • Send the real amount. Pass the order total in the currency you report in, so revenue per source is meaningful.
// On /thank-you
const key = "tracked_" + orderId;
if (!sessionStorage.getItem(key)) {
  window.hb?.track("purchase", { amount: orderTotal, plan: planName });
  sessionStorage.setItem(key, "1");
}

Reading the results

Once events come in, the useful views are:

  • Conversions by source. Which channels send people who sign up, not just people who visit? AI assistants and newsletters often punch above their weight.
  • Conversions by entry page. Which landing pages start the journeys that convert?
  • Individual journeys. Open a few converting visitors and read their path. You’ll spot the pages that do the persuading.

To turn counts into rates, divide conversions by visitors for the same segment. The free conversion rate calculator does this, and if you’re testing two versions of a page, the A/B test significance calculator tells you whether the difference is real. For paid campaigns, the ROAS calculator turns tracked revenue into return on ad spend.

A starter tracking plan

EventWhenProperties
signupAccount created (success screen)plan
purchasePayment confirmed (thank-you page)amount, plan
plan_clickedPricing plan button clickedplan, billing
cta_clickedMain call-to-action clickedlocation
lead_sentContact or demo form submittedform

Five events, all tied to decisions. Add more only when a question comes up that these can’t answer.

See your visitors live in a minute.

One line of code. No cookies. 7 days free.