WhatsApp Floating Button — Free HTML CSS JS Contact Widget Snippet

WhatsApp Floating Button · Buttons · Plain HTML, CSS & JS · Live preview

CategoryButtons

What's included

Features

Uses WhatsApp's official wa.me deep link format: no API key or SDK integration required
Pre-filled message is built with encodeURIComponent and can include the current page title
Business-hours check in a fixed time zone drives an online or offline state
Greeting bubble with avatar and status dot, dismissible by button or Escape key, remembered per session
Unread badge that clears once the bubble opens or the button is clicked
Pulse ring is a separate element, so the button itself never resizes; it stops when offline
Left or right placement via one data-position attribute
Safe-area inset keeps the button clear of the iPhone home indicator
prefers-reduced-motion disables the pulse and bubble animations
Analytics hook pushes a whatsapp_fab_click event to dataLayer
Real anchor with aria-label, focus-visible outline and rel="noopener noreferrer"
No framework, no chat widget library, no build step

About this UI Snippet

WhatsApp Floating Button — Contact Widget With Greeting Bubble and Business Hours

Screenshot of the WhatsApp Floating Button snippet rendered live

A floating WhatsApp button is one of the simplest, highest-impact additions to a small-business or support-facing website: a single fixed circular button that opens a pre-filled WhatsApp chat, no phone app plugin or chat SDK required. This version goes further than a bare icon: it adds an unread badge, a dismissible greeting bubble, and an online or offline status driven by your business hours.

How the WhatsApp link works

The button is a plain <a> tag pointing at a wa.me link: https://wa.me/15551234567?text=.... wa.me is WhatsApp's own official link format and needs no API key or app integration. The number after wa.me/ is the full international phone number with no +, spaces, or leading zeros. The ?text= parameter pre-fills the message box; the script builds it with encodeURIComponent, so spaces and special characters are always encoded correctly, and it swaps {page} for the current page title so your team knows which page the visitor was on.

Business hours and online status

isOnline() reads the current weekday and hour in a fixed time zone (CONFIG.hours.timeZone) using Intl.DateTimeFormat, so "9 to 6, Monday to Friday" means *your* team's hours, not the visitor's clock. When it's outside those hours, the status dot turns grey, the pulse ring stops, the unread badge hides, and the bubble asks visitors to leave a message. The pre-filled text also gets "(sent outside business hours)" appended, so whoever picks it up knows why the reply took a while.

The greeting bubble

After showAfterMs the bubble opens above the button with an avatar, the status line and a short message. The close button and the Escape key both dismiss it, and the dismissal is remembered in sessionStorage so it doesn't nag on every page of the same visit (wrapped in try/catch, so a blocked storage never breaks the widget).

How the pulsing ring works

Behind the button sits a second circle, .pulse-ring, sized identically via inset: 0 with z-index: -1. The button has its own stacking context, so the ring always renders behind the solid button. A @keyframes pulse animation scales it from 1 to 1.9 while fading from 0.6 to 0, so only the ring visibly pings outward and the button never changes size.

Left or right, and phone-friendly

The fixed wrapper carries data-position="right|left", and the bubble tail flips with it. bottom includes env(safe-area-inset-bottom) so the button never sits under the iPhone home indicator.

Respecting reduced motion

A prefers-reduced-motion: reduce query disables the pulse and the bubble animation for users who are sensitive to motion at the OS level. The widget stays fully functional.

Step by step

How to Use

  1. 1
    Load the snippetClick "WhatsApp Floating Button" in the sidebar Library tab. The preview shows the pulsing green button with an unread badge, then the greeting bubble opens after a couple of seconds.
  2. 2
    Set your phone numberIn the JS panel, change CONFIG.phone to your business number in international format: digits only, no + or leading zeros.
  3. 3
    Customize the messageEdit CONFIG.message. Use {page} to insert the current page title. The script URL-encodes the text for you.
  4. 4
    Set your business hoursEdit CONFIG.hours (time zone, days and from/to hours). Outside those hours the widget shows as offline.
  5. 5
    Try the demo controlsUse the Online/Offline, Left/Right and Replay greeting buttons to preview each state, then delete those buttons and the "Demo-only controls" code at the bottom of the JS.
  6. 6
    Adjust position and animationSwitch data-position between right and left, change bottom/right offsets in .wa-widget, or tune the pulse keyframes' scale and opacity.
  7. 7
    Export in your formatClick "HTML" for a standalone file, "JSX" for React, or "Tailwind" for React + Tailwind CSS.

Real-world uses

Common Use Cases

CHAT
Small business and support contact
Give website visitors a one-tap way to start a WhatsApp conversation with your business, without needing a full chat widget SDK.
Set expectations with business hours
Show visitors honestly when your team is away, and tag out-of-hours messages so your team can prioritise replies.
Pre-fill context per page
Use the {page} placeholder, or build the text from a product name, so every conversation starts with the context your team needs.
Learn CSS-only pulse/ping animations
Study how a second absolutely-positioned element with its own keyframe animation creates a radiating ring independent of the parent button's size.
Match your brand's floating action style
Adjust the button color, size, bubble and pulse ring to match other floating elements already on your site.
Respect motion sensitivity preferences
The prefers-reduced-motion query here is a good reference pattern for any other decorative CSS animation on your site.

Got questions?

Frequently Asked Questions

No. This uses wa.me, WhatsApp's free official click-to-chat link format that works with any regular or Business WhatsApp account. No API key, approval process, or paid integration is required.

Use the full number in international format with the country code, but with no plus sign, spaces, dashes, or leading zeros. For example 15551234567 for a US number starting with +1.

Edit CONFIG.message in the JavaScript. The script URL-encodes it with encodeURIComponent and appends it as ?text= on the wa.me link, so spaces and special characters are handled for you. Use {page} to insert the current page title.

isOnline() checks the current weekday and hour in the time zone set in CONFIG.hours using Intl.DateTimeFormat, then compares them with your days and from/to hours. It uses your team's time zone, not the visitor's clock. Outside those hours the dot turns grey, the pulse stops and the bubble asks for a message.

No. It only reflects the schedule you configure; WhatsApp does not expose agent presence to a website. Treat it as a hint about expected response time rather than live availability.

Yes, with no special detection code. On mobile it opens the native WhatsApp app if installed; on desktop it opens WhatsApp Web in a new tab, or the desktop app if the OS handles wa.me links.

Dismissing it stores a flag in sessionStorage, so it stays closed for the rest of the visit. Switch to localStorage in wasDismissed() and closeBubble() to remember the choice across visits, or set showAfterMs very high or remove the setTimeout line to never auto-open it.

Yes. The button is a real <a> element with a descriptive aria-label, reachable via Tab, with a visible focus-visible outline. The bubble is a labelled dialog, can be closed with its button or the Escape key, and decorative icons are hidden from assistive technology.

Yes. Add a scroll listener that toggles a hidden class on .wa-widget based on window.scrollY, the same pattern used by a back-to-top button.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Give this snippet's HTML, CSS and JS to an AI coding assistant like Claude and ask it to adapt the CONFIG block to your business: your number, your time zone and opening hours, and a message that mentions the page or product. It's also a good prompt for extending the widget, for example opening hours per weekday, a second department number, remembering dismissal in localStorage, showing the button only after the visitor scrolls, or turning it into a React or Vue component. Ask it to double check the wa.me URL-encoding for any special characters in your message.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a floating WhatsApp contact widget in plain HTML, CSS and JavaScript, with no chat library or SDK.

Requirements:
- A circular green button fixed to a bottom corner (data-position="right|left") as a real anchor tag pointing at a wa.me link, with an inline SVG WhatsApp logo, an aria-label, a focus-visible outline and rel="noopener noreferrer". Include env(safe-area-inset-bottom) in its bottom offset.
- A CONFIG object with phone (international digits only), message (supports a {page} placeholder), showAfterMs, business hours (timeZone, days, from, to) and a storageKey. Build the href with encodeURIComponent.
- isOnline() uses Intl.DateTimeFormat with the configured time zone to decide whether the team is in business hours. Offline state: grey status dot, no pulse, no unread badge, a "leave a message" greeting, and "(sent outside business hours)" appended to the pre-filled text.
- A pulsing ring: a second absolutely positioned circle behind the button (the button creates its own stacking context) that scales up and fades out on an infinite keyframe loop without resizing the button.
- A red unread badge on the button, and a greeting bubble above it with an avatar, name, status line and message. The bubble opens after showAfterMs, closes with its close button or the Escape key, and remembers dismissal in sessionStorage inside try/catch.
- A click handler that pushes a whatsapp_fab_click event to window.dataLayer.
- Wrap the pulse and bubble animations in a prefers-reduced-motion media query.
- Add small demo-only controls (online/offline, left/right, replay greeting) clearly marked for deletion.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="page-demo">
  <h1>Contact page</h1>
  <p>A floating WhatsApp button stays fixed in the corner of the viewport, with a pulsing ring, an unread badge and a greeting bubble that shows whether your team is online.</p>
  <div class="wa-controls">
    <button type="button" id="ctlOnline">Online</button>
    <button type="button" id="ctlSide">Right</button>
    <button type="button" id="ctlReplay">Replay greeting</button>
  </div>
</div>

<div class="wa-widget" id="waWidget" data-position="right">
  <div class="wa-bubble" id="waBubble" role="dialog" aria-label="Chat with us on WhatsApp" hidden>
    <button class="wa-close" id="waClose" type="button" aria-label="Dismiss chat prompt">&times;</button>
    <div class="wa-head">
      <span class="wa-avatar" aria-hidden="true">S</span>
      <div class="wa-who">
        <strong>Support team</strong>
        <span class="wa-status"><i class="wa-dot" id="waDot"></i><span id="waStatus">Online now</span></span>
      </div>
    </div>
    <p class="wa-msg" id="waMsg">Hi 👋 Have a question? Chat with us on WhatsApp.</p>
  </div>

  <a class="whatsapp-fab" id="waFab" href="https://wa.me/15551234567?text=Hi!%20I%20have%20a%20question" target="_blank" rel="noopener noreferrer" aria-label="Chat with us on WhatsApp">
    <span class="pulse-ring"></span>
    <svg viewBox="0 0 32 32" width="28" height="28" fill="#fff" aria-hidden="true">
      <path d="M16.004 3C9.373 3 4 8.373 4 15.004c0 2.65.86 5.1 2.32 7.09L4.5 28.5l6.58-1.79a11.9 11.9 0 0 0 4.92 1.07h.005c6.63 0 12.004-5.373 12.004-12.004S22.634 3 16.004 3zm0 21.8h-.004a9.78 9.78 0 0 1-4.98-1.36l-.357-.212-3.68 1 .984-3.586-.233-.368a9.77 9.77 0 0 1-1.5-5.27c0-5.4 4.396-9.8 9.8-9.8 2.617 0 5.077 1.02 6.93 2.87a9.73 9.73 0 0 1 2.868 6.93c0 5.4-4.397 9.796-9.828 9.796zm5.36-7.34c-.293-.147-1.735-.856-2.004-.953-.27-.098-.466-.147-.663.147-.196.293-.76.953-.932 1.15-.172.196-.343.22-.636.073-.293-.147-1.238-.456-2.358-1.454-.872-.777-1.46-1.737-1.632-2.03-.172-.293-.018-.452.13-.598.133-.132.293-.343.44-.514.146-.172.195-.294.293-.49.098-.196.049-.368-.024-.514-.073-.147-.663-1.6-.91-2.19-.24-.575-.482-.497-.663-.507l-.564-.01c-.196 0-.514.073-.783.368-.27.294-1.03 1.007-1.03 2.456 0 1.45 1.055 2.85 1.202 3.046.147.196 2.076 3.17 5.03 4.443.703.303 1.25.484 1.678.62.705.224 1.347.192 1.855.117.566-.084 1.735-.71 1.98-1.395.245-.686.245-1.273.172-1.395-.073-.123-.27-.196-.563-.343z"/>
    </svg>
    <span class="wa-badge" id="waBadge" aria-hidden="true">1</span>
  </a>
</div>