<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nazir Abbas</title>
    <description>The latest articles on DEV Community by Nazir Abbas (@nazirabbas).</description>
    <link>https://dev.to/nazirabbas</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4033312%2Fbc758445-944d-4ed8-a807-66ceacbd298d.png</url>
      <title>DEV Community: Nazir Abbas</title>
      <link>https://dev.to/nazirabbas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nazirabbas"/>
    <language>en</language>
    <item>
      <title>I shipped a 5.7 KB zero-dep WhatsApp CTA library for Next.js</title>
      <dc:creator>Nazir Abbas</dc:creator>
      <pubDate>Fri, 17 Jul 2026 07:21:15 +0000</pubDate>
      <link>https://dev.to/nazirabbas/i-shipped-a-57-kb-zero-dep-whatsapp-cta-library-for-nextjs-5cd6</link>
      <guid>https://dev.to/nazirabbas/i-shipped-a-57-kb-zero-dep-whatsapp-cta-library-for-nextjs-5cd6</guid>
      <description>&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody fills out the contact form. They message on WhatsApp.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;nextjs-whatsapp-cta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/nextjs-whatsapp-cta" rel="noopener noreferrer"&gt;nextjs-whatsapp-cta&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/nazirabas/nextjs-whatsapp-cta" rel="noopener noreferrer"&gt;nazirabas/nextjs-whatsapp-cta&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below, the design decisions I made and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with existing WhatsApp libs
&lt;/h2&gt;

&lt;p&gt;I checked npm before writing my own. The top results fell into three buckets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Example issue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Massive UI kits&lt;/td&gt;
&lt;td&gt;200 KB import to render one green pill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Component libraries&lt;/td&gt;
&lt;td&gt;Baked-in styling that fights your Tailwind setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Simple" libs&lt;/td&gt;
&lt;td&gt;Pulls in moment, axios, or lodash as transitive deps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None handled the two things I actually needed:&lt;/p&gt;

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

&lt;p&gt;So I wrote three files.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is in the package
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── lib.ts               // buildWhatsAppUrl, trackWhatsAppClick, normalizePhone
├── WhatsAppButton.tsx   // inline pill CTA (Tailwind class-based)
└── WhatsAppFloating.tsx // site-wide floating widget (inline styles)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;200 lines total. Zero runtime deps. Peers: react and react-dom (&amp;gt;= 18).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tarball is 5.7 KB. Unpacked 23.6 KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WhatsAppButton&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;WhatsAppFloating&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nextjs-whatsapp-cta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Inline CTA on a hero section&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WhatsAppButton&lt;/span&gt;
  &lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"+971501824077"&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Hi, I'd like to book the Sunseeker 62 for Sunday."&lt;/span&gt;
  &lt;span class="na"&gt;utm&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;website&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;medium&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hero&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;campaign&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;summer-2026&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;trackingLabel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hero-cta"&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  Book on WhatsApp
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;WhatsAppButton&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;// Site-wide floating widget in root layout&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WhatsAppFloating&lt;/span&gt;
  &lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"+971501824077"&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Hi, coming from your website."&lt;/span&gt;
  &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bottom-right"&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click either one, and if window.gtag exists this fires automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;gtag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;whatsapp_click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;event_label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hero-cta&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No provider wrap. No useEffect. No GA4 SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three design choices worth explaining
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. UTMs go in the message body, not the URL
&lt;/h3&gt;

&lt;p&gt;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 (&lt;code&gt;(utm_source=website&amp;amp;utm_medium=hero)&lt;/code&gt; at the bottom), but it survives forwarding, screenshots, and cross-device sync.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From src/lib.ts&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;utm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;utmString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;utm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`utm_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;utmString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n\n(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;utmString&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Phone normalization is aggressive
&lt;/h3&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizePhone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^\d]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accepts &lt;code&gt;+971 50 182 4077&lt;/code&gt;, &lt;code&gt;971501824077&lt;/code&gt;, &lt;code&gt;+971-50-182-4077&lt;/code&gt;. Outputs the same thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The floating widget is not lazy-loaded
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I intentionally left out
&lt;/h2&gt;

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

&lt;p&gt;If you want any of those, fork it. It is MIT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open source it
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;nextjs-whatsapp-cta
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/nextjs-whatsapp-cta" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/nextjs-whatsapp-cta&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/nazirabas/nextjs-whatsapp-cta" rel="noopener noreferrer"&gt;https://github.com/nazirabas/nextjs-whatsapp-cta&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;License: MIT&lt;/li&gt;
&lt;/ul&gt;

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




&lt;p&gt;&lt;strong&gt;About me:&lt;/strong&gt; I am Nazir Abbas, a web developer and SEO specialist based in Dubai. Founder of &lt;a href="https://www.upwork.com/agencies/digisoftrank" rel="noopener noreferrer"&gt;Digi-Soft-Rank&lt;/a&gt;, building premium websites for luxury brands across Dubai, Monaco, Ibiza, and Malta. Say hi on &lt;a href="https://www.linkedin.com/in/nazir-abbas/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; or &lt;a href="https://cal.com/nazir-abbas" rel="noopener noreferrer"&gt;book a call&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
