Navigation & Accessibility

Free Sticky Mobile CTA Widget

A bottom call-to-action bar that only shows on phones and small screens.

A slim bar pinned to the bottom of the screen with a single bold button — perfect for “Call now”, “Book a quote”, or “Shop the sale” on mobile. It only appears on viewports under your chosen breakpoint (768px by default), so desktop visitors never see it. Install it with one script tag just before your closing </body> tag.

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/sticky-mobile-cta.js" data-hl-text="Book Your Free Consultation" data-hl-link="https://hayneslab.dev/contact" async></script>
<!-- Free Sticky Mobile CTA widget by haynes lab — https://hayneslab.dev/tools/widgets/sticky-mobile-cta -->
Inline (self-contained)html
<!-- Free Sticky Mobile CTA widget by haynes lab — https://hayneslab.dev/tools/widgets/sticky-mobile-cta -->
<script data-hl-text="Book Your Free Consultation" data-hl-link="https://hayneslab.dev/contact">
/*!
 * haynes lab — Sticky Mobile CTA widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/sticky-mobile-cta
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-sticky-mobile-cta';
  var STYLE_ID = 'hlw-sticky-mobile-cta-styles';

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/sticky-mobile-cta.js"]');
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: fixed; left: 0; right: 0; bottom: 0; z-index: 99998;' +
      ' display: flex; flex-direction: column; align-items: center; gap: 4px;' +
      ' padding: 12px 16px calc(10px + env(safe-area-inset-bottom, 0px));' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 16px; line-height: 1.4; box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.15); }' +
      '.' + PREFIX + '--in-stage { position: absolute; z-index: 10; }' +
      '.' + PREFIX + '__button { display: flex; align-items: center; justify-content: center;' +
      ' width: 100%; max-width: 480px; padding: 13px 24px; border: 0; border-radius: 10px;' +
      ' background: #ffffff; font-family: inherit; font-size: 16px; font-weight: 700;' +
      ' line-height: 1.2; text-align: center; text-decoration: none; cursor: pointer;' +
      ' box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12); transition: transform 0.15s ease, box-shadow 0.15s ease; }' +
      '.' + PREFIX + '__button:hover { transform: translateY(-1px); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.18); }' +
      '.' + PREFIX + '__button:focus-visible { outline: 2px solid #ffffff; outline-offset: 2px; }' +
      '.' + PREFIX + '__credit { font-size: 11px; line-height: 1; display: inline-flex;' +
      ' align-items: center; gap: 4px; color: inherit; opacity: 0.65; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { opacity: 0.9; text-decoration: underline; }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  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(script) {
    var text = script.getAttribute('data-hl-text') || 'Get a Free Quote';
    var link = script.getAttribute('data-hl-link') || '#';
    var bg = script.getAttribute('data-hl-bg') || '#f97316';
    var breakpoint = parseInt(script.getAttribute('data-hl-breakpoint'), 10);
    if (isNaN(breakpoint) || breakpoint <= 0) breakpoint = 768;

    var stage = script.closest('.widget-demo__stage');

    injectStyles();

    var root = el('div', PREFIX);
    root.setAttribute('role', 'region');
    root.setAttribute('aria-label', 'Call to action');
    root.style.backgroundColor = bg;
    root.style.color = '#ffffff';

    var button = el('a', PREFIX + '__button', text);
    button.href = link;
    button.style.color = bg;
    root.appendChild(button);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/sticky-mobile-cta';
    credit.target = '_blank';
    credit.rel = 'noopener';
    root.appendChild(credit);

    if (stage) {
      // Landing-page demo: render always, pinned to the bottom of the stage.
      root.classList.add(PREFIX + '--in-stage');
      stage.appendChild(root);
      return;
    }

    // Live embed: fixed to the viewport bottom, visible only under the breakpoint.
    document.body.appendChild(root);

    function sync() {
      var visible = window.matchMedia('(max-width: ' + (breakpoint - 1) + 'px)').matches;
      root.style.display = visible ? '' : 'none';
      document.documentElement.style.paddingBottom = visible ? root.offsetHeight + 'px' : '';
    }

    var mq = window.matchMedia('(max-width: ' + (breakpoint - 1) + 'px)');
    if (mq.addEventListener) {
      mq.addEventListener('change', sync);
    } else if (mq.addListener) {
      mq.addListener(sync);
    }
    window.addEventListener('resize', sync);
    sync();
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab sticky-mobile-cta] Could not locate its own script tag. Nothing rendered.');
      return;
    }
    build(script);
  }

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

</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-text"Get a Free Quote"Label shown on the call-to-action button.
data-hl-link"#"URL the button opens — a page, tel: number, or mailto: address.
data-hl-bg"#f97316"Background color of the bar; the button renders white with matching text.
data-hl-breakpoint"768"The bar only appears on viewports narrower than this width in pixels.

Frequently asked questions

How do I install the Sticky Mobile CTA?

Paste the script snippet just before your closing </body> tag. The bar fixes itself to the bottom of the screen and only shows on phones and small tablets — nothing renders on desktop.

Can I change the text, link, colors, and when it appears?

Yes — set data-hl-text for the button label, data-hl-link for the destination (a URL, tel: number, or mailto:), data-hl-bg for the bar color, and data-hl-breakpoint to control the maximum viewport width where the bar is visible (default 768px).

Does it work on WordPress, Shopify, or Squarespace?

Yes — it is a single vanilla JS file with no dependencies, so it works on WordPress, Shopify, Squarespace, or any site that can embed a script tag.

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