Forms & Lead Capture

Free Feedback Widget Widget

A floating side tab that opens a quick rating and comment form.

A floating "Feedback" tab that sits on the edge of every page and slides out a small form with a rating select and comment box. Submissions POST to any endpoint you choose (Formspree, Basin, or your own API). Install is one script tag before the 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/feedback-widget.js" data-hl-action="https://formspree.io/f/your-form-id" data-hl-title="How are we doing?" async></script>
<!-- Free Feedback Widget widget by haynes lab — https://hayneslab.dev/tools/widgets/feedback-widget -->
Inline (self-contained)html
<!-- Free Feedback Widget widget by haynes lab — https://hayneslab.dev/tools/widgets/feedback-widget -->
<script data-hl-action="https://formspree.io/f/your-form-id" data-hl-title="How are we doing?">
/*!
 * haynes lab — Feedback Widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/feedback-widget
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-feedback-widget';
  var STYLE_ID = 'hlw-feedback-widget-styles';

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

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: fixed; right: 0; top: 50%; z-index: 99999;' +
      ' transform: translate(100%, -50%); transition: transform 0.35s ease;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 16px; line-height: 1.5; color: #1f2937; }' +
      '.' + PREFIX + '--open { transform: translate(0, -50%); }' +
      '.' + PREFIX + '--staged { position: absolute; z-index: 10; }' +
      '.' + PREFIX + '__tab { position: absolute; right: 100%; top: 50%; transform: translateY(-50%);' +
      ' display: flex; align-items: center; padding: 14px 8px; border: 0; cursor: pointer;' +
      ' background: var(--hlw-accent); color: #ffffff; font: inherit; font-size: 13px; font-weight: 600;' +
      ' letter-spacing: 0.04em; writing-mode: vertical-rl;' +
      ' border-radius: 8px 0 0 8px; box-shadow: -2px 0 10px rgba(0, 0, 0, 0.15); }' +
      '.' + PREFIX + '__tab:hover { filter: brightness(1.08); }' +
      '.' + PREFIX + '__tab:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__panel { position: relative; width: 300px; max-width: 88vw; padding: 24px 22px 34px;' +
      ' background: #ffffff; border: 1px solid #e5e7eb; border-right: 0; border-radius: 12px 0 0 12px;' +
      ' box-shadow: -8px 4px 24px rgba(0, 0, 0, 0.14); }' +
      '.' + PREFIX + '__close { position: absolute; top: 8px; right: 8px; width: 28px; height: 28px;' +
      ' border: 0; border-radius: 50%; background: transparent; color: #9ca3af; font-size: 18px;' +
      ' line-height: 1; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; }' +
      '.' + PREFIX + '__close:hover { background: #f3f4f6; color: #4b5563; }' +
      '.' + PREFIX + '__close:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__title { display: block; font-size: 17px; font-weight: 700; color: #111827; margin: 0 24px 14px 0; }' +
      '.' + PREFIX + '__field { display: block; margin-bottom: 12px; }' +
      '.' + PREFIX + '__label { display: block; font-size: 13px; font-weight: 600; color: #374151; margin-bottom: 5px; }' +
      '.' + PREFIX + '__input { display: block; width: 100%; font: inherit; font-size: 14px; padding: 9px 11px;' +
      ' border: 1px solid #d1d5db; border-radius: 8px; background: #ffffff; color: #111827; }' +
      '.' + PREFIX + '__input:focus { outline: 2px solid var(--hlw-accent); outline-offset: 1px; border-color: var(--hlw-accent); }' +
      'textarea.' + PREFIX + '__input { min-height: 84px; resize: vertical; }' +
      '.' + PREFIX + '__btn { display: block; width: 100%; font: inherit; font-size: 14px; font-weight: 600;' +
      ' padding: 10px 16px; margin-top: 2px; border: 0; border-radius: 8px; background: var(--hlw-accent);' +
      ' color: #ffffff; cursor: pointer; }' +
      '.' + PREFIX + '__btn:hover { filter: brightness(1.08); }' +
      '.' + PREFIX + '__btn:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__btn:disabled { opacity: 0.6; cursor: default; }' +
      '.' + PREFIX + '__msg { display: none; font-size: 13px; margin: 10px 0 0; }' +
      '.' + PREFIX + '__msg[data-show] { display: block; }' +
      '.' + PREFIX + '__msg[data-kind="error"] { color: #b91c1c; }' +
      '.' + PREFIX + '__success { padding: 18px 4px; text-align: center; }' +
      '.' + PREFIX + '__success-icon { display: inline-flex; align-items: center; justify-content: center;' +
      ' width: 40px; height: 40px; margin-bottom: 10px; border-radius: 50%; background: var(--hlw-accent);' +
      ' color: #ffffff; font-size: 20px; line-height: 1; }' +
      '.' + PREFIX + '__success-text { font-size: 14px; color: #374151; margin: 0; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 8px; font-size: 11px;' +
      ' display: inline-flex; align-items: center; gap: 4px; color: #9ca3af; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; 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 action = script.getAttribute('data-hl-action');
    if (!action) {
      console.warn('[haynes lab feedback-widget] data-hl-action is required (URL the form POSTs to). Nothing rendered.');
      return;
    }
    var buttonLabel = script.getAttribute('data-hl-button-label') || 'Feedback';
    var title = script.getAttribute('data-hl-title') || 'Send us feedback';
    var accent = script.getAttribute('data-hl-accent') || '#f97316';

    injectStyles();

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

    var root = el('div', PREFIX + (stage ? ' ' + PREFIX + '--staged' : ''));
    root.style.setProperty('--hlw-accent', accent);

    var tab = el('button', PREFIX + '__tab', buttonLabel);
    tab.type = 'button';
    tab.setAttribute('aria-expanded', 'false');
    tab.setAttribute('aria-label', buttonLabel + ' — open feedback form');

    var panel = el('div', PREFIX + '__panel');
    panel.setAttribute('role', 'dialog');
    panel.setAttribute('aria-label', title);

    var close = el('button', PREFIX + '__close', '×');
    close.type = 'button';
    close.setAttribute('aria-label', 'Close feedback form');
    panel.appendChild(close);

    panel.appendChild(el('span', PREFIX + '__title', title));

    var form = document.createElement('form');
    form.noValidate = true;

    var ratingField = el('label', PREFIX + '__field');
    ratingField.appendChild(el('span', PREFIX + '__label', 'How would you rate your experience?'));
    var ratingSelect = el('select', PREFIX + '__input');
    ratingSelect.name = 'rating';
    var placeholder = document.createElement('option');
    placeholder.value = '';
    placeholder.textContent = 'Select a rating';
    ratingSelect.appendChild(placeholder);
    [['5', '5 — Excellent'], ['4', '4 — Good'], ['3', '3 — Okay'], ['2', '2 — Poor'], ['1', '1 — Terrible']]
      .forEach(function (pair) {
        var opt = document.createElement('option');
        opt.value = pair[0];
        opt.textContent = pair[1];
        ratingSelect.appendChild(opt);
      });
    ratingField.appendChild(ratingSelect);
    form.appendChild(ratingField);

    var commentField = el('label', PREFIX + '__field');
    commentField.appendChild(el('span', PREFIX + '__label', 'Comments (optional)'));
    var commentInput = el('textarea', PREFIX + '__input');
    commentInput.name = 'comment';
    commentInput.placeholder = 'Tell us what worked or what we could improve…';
    commentField.appendChild(commentInput);
    form.appendChild(commentField);

    var btn = el('button', PREFIX + '__btn', 'Send feedback');
    btn.type = 'submit';
    form.appendChild(btn);

    var msg = el('p', PREFIX + '__msg');
    msg.setAttribute('role', 'status');
    msg.setAttribute('aria-live', 'polite');

    function showError(text, input) {
      msg.textContent = text;
      msg.setAttribute('data-kind', 'error');
      msg.setAttribute('data-show', '');
      if (input) input.focus();
    }

    var open = false;
    function setOpen(next) {
      open = next;
      root.classList.toggle(PREFIX + '--open', open);
      tab.setAttribute('aria-expanded', open ? 'true' : 'false');
      if (open) ratingSelect.focus();
      else tab.focus();
    }
    function onKeydown(e) {
      if (e.key === 'Escape' && open) setOpen(false);
    }
    tab.addEventListener('click', function () { setOpen(!open); });
    close.addEventListener('click', function () { setOpen(false); });
    document.addEventListener('keydown', onKeydown);

    form.addEventListener('submit', function (e) {
      e.preventDefault();
      var rating = ratingSelect.value;
      if (!rating) return showError('Please select a rating.', ratingSelect);

      btn.disabled = true;
      var body = new FormData();
      body.append('rating', rating);
      body.append('comment', commentInput.value.trim());
      fetch(action, { method: 'POST', body: body, headers: { Accept: 'application/json' } })
        .then(function (res) {
          if (res.ok) {
            form.style.display = 'none';
            msg.style.display = 'none';
            var success = el('div', PREFIX + '__success');
            success.setAttribute('role', 'status');
            success.appendChild(el('span', PREFIX + '__success-icon', '✓'));
            success.appendChild(el('p', PREFIX + '__success-text', 'Thanks for your feedback!'));
            panel.appendChild(success);
            setTimeout(function () { setOpen(false); }, 2500);
          } else {
            btn.disabled = false;
            showError('Something went wrong — please try again.');
          }
        })
        .catch(function () {
          btn.disabled = false;
          showError('Something went wrong — please try again.');
        });
    });

    panel.appendChild(form);
    panel.appendChild(msg);

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

    root.appendChild(tab);
    root.appendChild(panel);

    // Landing-page demo: the stage is positioned relative, so scope the
    // otherwise-fixed widget to it via the --staged (absolute) modifier.
    if (stage) {
      stage.appendChild(root);
    } else {
      document.body.appendChild(root);
    }
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab feedback-widget] 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-action(required)URL the form POSTs to (Formspree, Basin, or your own endpoint). The widget does not render without it.
data-hl-button-label"Feedback"Text shown on the floating side tab that opens the form.
data-hl-title"Send us feedback"Heading displayed at the top of the slide-out form.
data-hl-accent"#f97316"Any CSS color for the side tab, submit button, focus outlines, and success icon.

Frequently asked questions

How do I install the feedback widget?

Paste the embed snippet just before the closing </body> tag — the tab floats on the right edge of every page. Set data-hl-action to the URL that should receive submissions (a Formspree/Basin form endpoint or your own API) and you are done.

How do I customize the tab text, heading, and color?

Everything is controlled through data-hl-* attributes on the script tag: data-hl-button-label for the side tab text, data-hl-title for the form heading, and data-hl-accent for the tab, button, and highlight color. No CSS editing required.

Does it work on WordPress, Shopify, or Squarespace?

Yes. The widget is a single dependency-free vanilla JS file, so it runs anywhere you can paste a script tag — WordPress (Custom HTML block), Shopify (theme.liquid), Squarespace (code injection), Webflow, or a plain HTML site.

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