DEV Community

Cover image for I shipped a 5.7 KB zero-dep WhatsApp CTA library for Next.js
Nazir Abbas
Nazir Abbas

Posted on • Originally published at github.com

I shipped a 5.7 KB zero-dep WhatsApp CTA library for Next.js

I build websites for luxury service brands. Yacht charters in Ibiza and Malta, private clinics in Dubai, perfume houses in Monaco. Different clients, same conversion pattern:

Nobody fills out the contact form. They message on WhatsApp.

After the tenth time I copy-pasted the same three files between projects, I extracted them into an npm package. This week I published it.

npm install nextjs-whatsapp-cta
Enter fullscreen mode Exit fullscreen mode

Below, the design decisions I made and why.

The problem with existing WhatsApp libs

I checked npm before writing my own. The top results fell into three buckets:

Category Example issue
Massive UI kits 200 KB import to render one green pill
Component libraries Baked-in styling that fights your Tailwind setup
"Simple" libs Pulls in moment, axios, or lodash as transitive deps

None handled the two things I actually needed:

  1. UTM tracking that survives the WhatsApp jump. Attribution must land in the message body, because query params are stripped on some WhatsApp clients.
  2. GA4 event firing without a provider wrapper. If window.gtag exists, fire the event. If not, no-op. No wrapping component, no SDK import.

So I wrote three files.

What is in the package

src/
├── lib.ts               // buildWhatsAppUrl, trackWhatsAppClick, normalizePhone
├── WhatsAppButton.tsx   // inline pill CTA (Tailwind class-based)
└── WhatsAppFloating.tsx // site-wide floating widget (inline styles)
Enter fullscreen mode Exit fullscreen mode

200 lines total. Zero runtime deps. Peers: react and react-dom (>= 18).

Tarball is 5.7 KB. Unpacked 23.6 KB.

Usage

import { WhatsAppButton, WhatsAppFloating } from "nextjs-whatsapp-cta";

// Inline CTA on a hero section
<WhatsAppButton
  phone="+971501824077"
  message="Hi, I'd like to book the Sunseeker 62 for Sunday."
  utm={{ source: "website", medium: "hero", campaign: "summer-2026" }}
  trackingLabel="hero-cta"
>
  Book on WhatsApp
</WhatsAppButton>

// Site-wide floating widget in root layout
<WhatsAppFloating
  phone="+971501824077"
  message="Hi, coming from your website."
  position="bottom-right"
/>
Enter fullscreen mode Exit fullscreen mode

Click either one, and if window.gtag exists this fires automatically:

gtag("event", "whatsapp_click", { event_label: "hero-cta" });
Enter fullscreen mode Exit fullscreen mode

No provider wrap. No useEffect. No GA4 SDK.

Three design choices worth explaining

1. UTMs go in the message body, not the URL

WhatsApp strips query params on some clients. If your CRM relies on knowing which ad triggered a chat, the only reliable place is inside the pre-filled message. It looks a little ugly ((utm_source=website&utm_medium=hero) at the bottom), but it survives forwarding, screenshots, and cross-device sync.

// From src/lib.ts
if (utm) {
  const utmString = Object.entries(utm)
    .filter(([, v]) => Boolean(v))
    .map(([k, v]) => `utm_${k}=${v}`)
    .join("&");
  if (utmString) body = `${message}\n\n(${utmString})`;
}
Enter fullscreen mode Exit fullscreen mode

2. Phone normalization is aggressive

Every production site I have inherited has the same number formatted three different ways across three CTAs. My URL builder strips everything except digits and always produces the same canonical wa.me link.

export function normalizePhone(phone: string): string {
  return phone.replace(/[^\d]/g, "");
}
Enter fullscreen mode Exit fullscreen mode

Accepts +971 50 182 4077, 971501824077, +971-50-182-4077. Outputs the same thing.

3. The floating widget is not lazy-loaded

For every other component I would say "of course lazy-load it." Not this one. The floating WhatsApp button is often the primary CTA on service-brand sites. It should render in first paint, not after your analytics bundle finishes loading.

It also uses inline styles (not Tailwind classes) so it renders correctly even on a marketing page that has not hydrated your design system yet.

What I intentionally left out

  • No auto-open on scroll. Dark pattern.
  • No sound effects. Please.
  • No pre-configured "hello" popup. The user came to your site, they know why they are here.
  • No custom modal wrapping WhatsApp. WhatsApp handles the chat surface, do not reinvent it.

If you want any of those, fork it. It is MIT.

Why open source it

Because 90% of my client work lives in private repos. Nothing to show on GitHub. This is my way of saying "here is how I actually think about conversion patterns" without leaking client work.

If you are a Next.js dev building for a service brand in a WhatsApp-first region (Middle East, LATAM, South Asia, Southern Europe), this saves you a day of scaffolding.

Install and try it

npm install nextjs-whatsapp-cta
Enter fullscreen mode Exit fullscreen mode

Issues and PRs welcome. If you use it in production, drop a comment. I would love to see how it looks in your project.


About me: I am Nazir Abbas, a web developer and SEO specialist based in Dubai. Founder of Digi-Soft-Rank, building premium websites for luxury brands across Dubai, Monaco, Ibiza, and Malta. Say hi on LinkedIn or book a call.

Top comments (0)