Business & Operations

Free Click-to-Call Button Widget

A floating call button that turns mobile visitors into phone calls.

A floating button that dials your number with one tap — the single highest-converting element a local service business can add. Optionally shows your business hours so callers know when you pick up. Sits in the corner, out of the way of your content.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/click-to-call.js" data-hl-phone="+18035550142" data-hl-label="Call for a free quote" data-hl-show-hours="true" data-hl-hours-text="Mon–Fri, 8am–6pm ET" async></script>
<!-- Free Click-to-Call Button widget by haynes lab — https://hayneslab.dev/tools/widgets/click-to-call -->
Inline (self-contained)html
<!-- Free Click-to-Call Button widget by haynes lab — https://hayneslab.dev/tools/widgets/click-to-call -->
<script data-hl-phone="+18035550142" data-hl-label="Call for a free quote" data-hl-show-hours="true" data-hl-hours-text="Mon–Fri, 8am–6pm ET">
/*!
 * haynes lab — Click-to-Call Button widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/click-to-call
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-click-to-call';
  var STYLE_ID = 'hlw-click-to-call-styles';

  // Capture the script element now (document.currentScript is null later for async/deferred).
  var script = document.currentScript ||
    document.querySelector('script[src*="widgets/click-to-call.js"]');
  if (!script) {
    console.warn('[haynes lab click-to-call] Could not locate the script element; widget not rendered.');
    return;
  }

  function readConfig() {
    var cfg = {};
    var json = script.getAttribute('data-hl-config');
    if (json) {
      try {
        var parsed = JSON.parse(json);
        for (var key in parsed) {
          if (!parsed.hasOwnProperty(key)) continue;
          // Accept both "label" and "data-hl-label" style keys.
          cfg[key.indexOf('data-hl-') === 0 ? key.slice(8) : key] = String(parsed[key]);
        }
      } catch (err) {
        console.warn('[haynes lab click-to-call] Malformed JSON in data-hl-config; ignoring it.', err);
      }
    }
    // Individual data-hl-* attributes override data-hl-config.
    var attrs = ['phone', 'label', 'position', 'show-hours', 'hours-text', 'bg'];
    for (var i = 0; i < attrs.length; i++) {
      var val = script.getAttribute('data-hl-' + attrs[i]);
      if (val !== null) cfg[attrs[i]] = val;
    }
    return cfg;
  }

  function sanitizeTel(raw) {
    var cleaned = String(raw).replace(/[^\d+]/g, '');
    // Keep at most a single leading "+".
    if (cleaned.charAt(0) === '+') {
      cleaned = '+' + cleaned.slice(1).replace(/\+/g, '');
    } else {
      cleaned = cleaned.replace(/\+/g, '');
    }
    return cleaned;
  }

  function friendlyPhone(raw) {
    var digits = String(raw).replace(/\D/g, '');
    var local = digits;
    if (digits.length === 11 && digits.charAt(0) === '1') {
      local = digits.slice(1); // North American country code
    }
    if (local.length === 10) {
      return '(' + local.slice(0, 3) + ') ' + local.slice(3, 6) + '-' + local.slice(6);
    }
    return String(raw); // Unknown format: show as given.
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ' { position: fixed; bottom: 16px; z-index: 2147483000; ' +
        'font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; ' +
        'font-size: 14px; line-height: 1.4; }' +
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + '--right { right: 16px; }' +
      '.' + PREFIX + '--left { left: 16px; }' +
      '.' + PREFIX + '__btn { display: inline-flex; align-items: center; gap: 10px; ' +
        'padding: 12px 18px; border-radius: 999px; text-decoration: none; color: #ffffff; ' +
        'background-color: var(--hlw-ctc-bg, #22c55e); ' +
        'box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25); ' +
        'transition: transform 0.15s ease, box-shadow 0.15s ease; }' +
      '.' + PREFIX + '__btn:hover { transform: translateY(-2px); ' +
        'box-shadow: 0 6px 18px rgba(0, 0, 0, 0.3); color: #ffffff; }' +
      '.' + PREFIX + '__btn:focus-visible { outline: 2px solid #111827; outline-offset: 2px; }' +
      '.' + PREFIX + '__icon { display: inline-flex; flex: 0 0 auto; }' +
      '.' + PREFIX + '__icon svg { display: block; }' +
      '.' + PREFIX + '__text { display: flex; flex-direction: column; text-align: left; }' +
      '.' + PREFIX + '__label { font-weight: 600; font-size: 14px; color: #ffffff; }' +
      '.' + PREFIX + '__hours { font-size: 11px; color: rgba(255, 255, 255, 0.85); }' +
      '.' + PREFIX + '__credit { display: flex; align-items: center; justify-content: center; gap: 4px; margin-top: 4px; font-size: 11px; ' +
        'color: #9ca3af; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }' +
      '@media (max-width: 480px) { .' + PREFIX + '__text { display: none; } ' +
        '.' + PREFIX + '__btn { padding: 12px; } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    (document.head || document.documentElement).appendChild(style);
  }

  function phoneIconSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('width', '20');
    svg.setAttribute('height', '20');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('fill', 'none');
    svg.setAttribute('aria-hidden', 'true');
    var path = document.createElementNS(svgNS, 'path');
    path.setAttribute('d',
      'M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 ' +
      'A19.79 19.79 0 0 1 2.08 4.18 2 2 0 0 1 4.06 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81 ' +
      'a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 ' +
      '2.81.7A2 2 0 0 1 22 16.92z');
    path.setAttribute('fill', 'currentColor');
    svg.appendChild(path);
    return svg;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function build(cfg) {
    // The script may run again (live demo re-injection, double embed) —
    // remove any previous instance so buttons never stack.
    var oldRoots = document.querySelectorAll('.' + PREFIX);
    for (var i = 0; i < oldRoots.length; i++) {
      oldRoots[i].parentNode.removeChild(oldRoots[i]);
    }

    var tel = sanitizeTel(cfg.phone);
    if (!tel.replace(/\D/g, '').length) {
      console.warn('[haynes lab click-to-call] Missing or invalid required attribute data-hl-phone ' +
        '(E.164, e.g. "+18035551234"). Widget not rendered.');
      return;
    }

    var position = cfg.position === 'bottom-left' ? 'bottom-left' : 'bottom-right';
    var label = cfg.label || 'Call us';
    var showHours = cfg['show-hours'] === 'true';
    var hoursText = cfg['hours-text'] || 'Mon\u2013Fri, 9am\u20135pm';
    var bg = cfg.bg || '#22c55e';

    injectStyles();

    var root = document.createElement('div');
    root.className = PREFIX + ' ' + PREFIX + (position === 'bottom-left' ? '--left' : '--right');
    root.style.setProperty('--hlw-ctc-bg', bg);

    var btn = document.createElement('a');
    btn.className = PREFIX + '__btn';
    btn.href = 'tel:' + tel;
    btn.setAttribute('aria-label', 'Call ' + friendlyPhone(cfg.phone));

    var icon = document.createElement('span');
    icon.className = PREFIX + '__icon';
    icon.appendChild(phoneIconSvg());
    btn.appendChild(icon);

    var text = document.createElement('span');
    text.className = PREFIX + '__text';

    var labelEl = document.createElement('span');
    labelEl.className = PREFIX + '__label';
    labelEl.textContent = label;
    text.appendChild(labelEl);

    if (showHours) {
      var hoursEl = document.createElement('span');
      hoursEl.className = PREFIX + '__hours';
      hoursEl.textContent = hoursText;
      text.appendChild(hoursEl);
    }
    btn.appendChild(text);
    root.appendChild(btn);

    var credit = document.createElement('a');
    credit.className = PREFIX + '__credit';
    credit.href = 'https://hayneslab.dev/tools/widgets/click-to-call';
    credit.target = '_blank';
    credit.rel = 'noopener';
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    root.appendChild(credit);

    // Floating widget: always appends to <body>.
    document.body.appendChild(root);
  }

  function init() {
    build(readConfig());
  }

  if (document.body) {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init, { once: true });
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-phone(required)Number dialed on tap, in E.164 format, e.g. "+18035551234". Displayed to visitors in a friendly format.
data-hl-label"Call us"Short text shown next to the phone icon on wider screens.
data-hl-position"bottom-right"Corner the button floats in: "bottom-right" or "bottom-left".
data-hl-show-hours"false"When "true", a small note under the label shows the text from data-hl-hours-text.
data-hl-hours-text"Mon–Fri, 9am–5pm"Short availability note displayed when data-hl-show-hours is "true".
data-hl-bg"#22c55e"Any CSS color for the button background.

Frequently asked questions

How do I install the click-to-call button?

Paste the embed snippet before your closing </body> tag and set data-hl-phone to your number in international format (for example +18035551234). The button floats in the corner of every page the script is on.

How do I move the button or change its color?

Set data-hl-position to "bottom-left" or "bottom-right" to choose the corner, and data-hl-bg to any CSS color to match your brand. You can also change the label text with data-hl-label.

Does it work on WordPress, Shopify, or Squarespace?

Yes. It is a dependency-free vanilla JS file that renders a floating button, so it works on any site where you can add a script tag — page builders included. On desktop it behaves like a normal tel: link.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch