Open demo

CSS code

HTML
<div><span class="lbl">Help widget with team intro and quick replies</span>
<div class="stage"><div class="page" aria-hidden="true"><i></i><i></i><i></i><i></i><b></b><i></i><i></i></div>
<div class="hw" data-widget>
  <section class="hw-panel" id="hw-panel-a" aria-labelledby="hw-title-a" data-panel>
    <div class="hw-head">
      <div class="hw-team" aria-hidden="true"><span>PS</span><span>OK</span><span>LB</span></div>
      <h2 id="hw-title-a">Lumen Support</h2>
      <p>Usually replies in under 5 minutes</p>
      <button class="hw-close" type="button" data-close><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg><span class="sr-only">Close chat</span></button>
    </div>
    <div class="hw-log" role="log" aria-live="polite" aria-labelledby="hw-title-a" tabindex="0" data-log>
      <div class="hw-msg"><p class="hw-who">Priya from Lumen</p><p class="hw-b"><span>Hi, I'm Priya. Ask me about an order, a return, or a lamp that won't pair with the app.</span> <time datetime="2026-10-14T10:30">10:30</time></p></div>
    </div>
    <div class="hw-typing" data-typing hidden><span class="hw-dots" aria-hidden="true"><i></i><i></i><i></i></span><span>Priya is typing</span></div>
    <div class="hw-quick" role="group" aria-label="Suggested questions" data-quick>
      <button type="button">Where is my order?</button><button type="button">Start a return</button><button type="button">Talk to a person</button>
    </div>
    <form class="hw-composer" data-composer>
      <label class="sr-only" for="hw-input-a">Message to Lumen Support</label>
      <textarea id="hw-input-a" rows="1" placeholder="Type your question" data-input></textarea>
      <button class="hw-send" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg><span class="sr-only">Send</span></button>
    </form>
    <template data-tpl="out"><div class="hw-msg out"><p class="hw-who sr-only">You</p><p class="hw-b"><span data-text></span> <time data-time></time></p></div></template>
    <template data-tpl="in"><div class="hw-msg"><p class="hw-who">Priya from Lumen</p><p class="hw-b"><span data-text></span> <time data-time></time></p></div></template>
  </section>
  <button class="hw-launcher" type="button" aria-expanded="true" aria-controls="hw-panel-a" data-launcher>
    <svg class="ic-open" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20.5l1.4-4.9A8 8 0 1 1 21 12z"/></svg>
    <svg class="ic-close" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
    <span class="sr-only">Support chat</span>
  </button>
</div></div></div>

<div><span class="lbl">Labelled launcher with an unread message</span>
<div class="stage"><div class="page" aria-hidden="true"><i></i><i></i><i></i><i></i><b></b><i></i><i></i></div>
<div class="hw" data-widget>
  <section class="hw-panel short" id="hw-panel-b" aria-labelledby="hw-title-b" data-panel hidden>
    <div class="hw-head">
      <div class="hw-team" aria-hidden="true"><span>OK</span></div>
      <h2 id="hw-title-b">Oskar from Lumen</h2>
      <p>Online now</p>
      <button class="hw-close" type="button" data-close><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg><span class="sr-only">Close chat</span></button>
    </div>
    <div class="hw-log" role="log" aria-live="polite" aria-labelledby="hw-title-b" tabindex="0" data-log>
      <div class="hw-msg"><p class="hw-who">Oskar from Lumen</p><p class="hw-b"><span>The Halo desk lamp comes in two heights, 38 and 52 cm. Happy to help you pick one.</span> <time datetime="2026-10-14T10:41">10:41</time></p></div>
    </div>
    <div class="hw-typing" data-typing hidden><span class="hw-dots" aria-hidden="true"><i></i><i></i><i></i></span><span>Oskar is typing</span></div>
    <form class="hw-composer" data-composer>
      <label class="sr-only" for="hw-input-b">Message to Oskar</label>
      <textarea id="hw-input-b" rows="1" placeholder="Type your question" data-input></textarea>
      <button class="hw-send" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg><span class="sr-only">Send</span></button>
    </form>
    <template data-tpl="out"><div class="hw-msg out"><p class="hw-who sr-only">You</p><p class="hw-b"><span data-text></span> <time data-time></time></p></div></template>
    <template data-tpl="in"><div class="hw-msg"><p class="hw-who">Oskar from Lumen</p><p class="hw-b"><span data-text></span> <time data-time></time></p></div></template>
  </section>
  <p class="hw-teaser"><span class="hw-av" aria-hidden="true">OK</span><span><b>Oskar from Lumen</b>The Halo desk lamp comes in two heights. Happy to help you pick one.</span></p>
  <button class="hw-launcher" type="button" aria-expanded="false" aria-controls="hw-panel-b" data-launcher>
    <svg class="ic-open" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20.5l1.4-4.9A8 8 0 1 1 21 12z"/></svg>
    <svg class="ic-close" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
    <span class="hw-label">Questions? Chat with us</span>
    <span class="hw-badge" data-badge>1<span class="sr-only"> new message</span></span>
  </button>
</div></div></div>
CSS
/* Support chat widget: a launcher button toggles a chat region (aria-expanded). Esc closes it and focus returns to the launcher.
   The dock is position:absolute inside the demo stage; use position:fixed to pin it to the viewport. */
.hw{--hw-accent:#6d28d9;--hw-accent-dark:#5b21b6;--hw-soft:#ede6ff;--hw-in:#f2f0f7;--hw-ink:#1d1830;--hw-muted:#5c5670;--hw-line:#e3def2;--hw-bg:#fff;
  position:absolute;right:20px;bottom:20px;left:20px;display:flex;flex-direction:column;align-items:flex-end;gap:14px;
  color:var(--hw-ink);font-size:.93rem;line-height:1.45;pointer-events:none}
.hw>*{pointer-events:auto}
.hw :where(h2,p){margin:0}
.hw button{font:inherit;cursor:pointer}
.hw .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.hw :focus-visible{outline:2px solid var(--hw-accent);outline-offset:2px}

/* panel */
.hw-panel{display:flex;flex-direction:column;width:min(370px,100%);height:500px;max-height:calc(100% - 0px);overflow:hidden;background:var(--hw-bg);
  border:1px solid var(--hw-line);border-radius:20px;box-shadow:0 28px 60px -24px rgba(45,24,99,.45);animation:hw-in .22s ease-out}
.hw-panel[hidden]{display:none}
.hw-panel.short{height:400px}
@keyframes hw-in{from{opacity:0;transform:translateY(10px) scale(.98)}}
.hw-head{position:relative;padding:18px 56px 18px 20px;background:linear-gradient(135deg,#6d28d9,#4c1d95);color:#fff}
.hw-team{display:flex;margin-bottom:10px}
.hw-team span{display:grid;place-items:center;width:34px;height:34px;margin-right:-8px;border:2px solid #5b21b6;border-radius:50%;background:#f5d0fe;color:#5b1470;font-size:.7rem;font-weight:700}
.hw-team span:nth-child(2){background:#bae6fd;color:#0c4a6e}
.hw-team span:nth-child(3){background:#fde68a;color:#713f12}
.hw-head h2{font-size:1.05rem;font-weight:700}
.hw-head p{margin-top:2px;font-size:.82rem;color:#ece6ff}
.hw-close{position:absolute;top:14px;right:14px;display:grid;place-items:center;width:34px;height:34px;border:0;border-radius:50%;background:rgba(255,255,255,.14);color:#fff}
.hw-close:hover{background:rgba(255,255,255,.26)}
.hw-head :focus-visible{outline-color:#fff}

/* log */
.hw-log{flex:1;display:flex;flex-direction:column;gap:12px;overflow-y:auto;padding:18px 16px}
.hw-log:focus-visible{outline-offset:-3px}
.hw-msg{display:flex;flex-direction:column;align-items:flex-start;gap:3px;max-width:86%}
.hw-msg.out{align-self:flex-end;align-items:flex-end}
.hw-who{margin-left:12px;font-size:.75rem;font-weight:600;color:var(--hw-muted)}
.hw-b{display:flex;flex-wrap:wrap;align-items:flex-end;column-gap:10px;padding:8px 13px 7px;border-radius:6px 18px 18px 18px;background:var(--hw-in);overflow-wrap:anywhere;white-space:pre-wrap}
.hw-msg.out .hw-b{border-radius:18px 6px 18px 18px;background:var(--hw-accent);color:#fff}
.hw-b time{margin-left:auto;font-size:.7rem;color:var(--hw-muted);white-space:nowrap}
.hw-msg.out .hw-b time{color:#e4d9ff}

/* typing indicator: three dots that hold still under reduced motion */
.hw-typing{display:flex;align-items:center;gap:8px;padding:0 16px 10px;font-size:.78rem;color:var(--hw-muted)}
.hw-typing[hidden]{display:none}
.hw-dots{display:flex;gap:3px;padding:8px 10px;border-radius:12px;background:var(--hw-in)}
.hw-dots i{width:6px;height:6px;border-radius:50%;background:var(--hw-muted);animation:hw-dot 1s infinite ease-in-out}
.hw-dots i:nth-child(2){animation-delay:.15s}.hw-dots i:nth-child(3){animation-delay:.3s}
@keyframes hw-dot{0%,60%,100%{transform:none;opacity:.5}30%{transform:translateY(-3px);opacity:1}}

/* quick replies */
.hw-quick{display:flex;flex-wrap:wrap;gap:6px;padding:0 16px 12px}
.hw-quick[hidden]{display:none}
.hw-quick button{padding:6px 12px;border:1.5px solid #a78bfa;border-radius:999px;background:var(--hw-bg);color:var(--hw-accent-dark);font-size:.82rem;font-weight:600}
.hw-quick button:hover{background:var(--hw-soft)}

/* composer */
.hw-composer{display:flex;align-items:flex-end;gap:8px;padding:10px 12px 12px;border-top:1px solid var(--hw-line)}
.hw-composer textarea{box-sizing:border-box;flex:1;min-width:0;max-height:110px;padding:10px 14px;border:1.5px solid #8f88a3;border-radius:16px;background:var(--hw-bg);color:var(--hw-ink);font:inherit;line-height:1.4;resize:none}
.hw-composer textarea::placeholder{color:#686279}
.hw-composer textarea:focus-visible{border-color:var(--hw-accent);outline-offset:1px}
.hw-send{flex:none;display:grid;place-items:center;width:42px;height:42px;border:0;border-radius:14px;background:var(--hw-accent);color:#fff}
.hw-send:hover{background:var(--hw-accent-dark)}
.hw-send:disabled{background:#d9d4e6;color:#4f4963;cursor:not-allowed}

/* teaser: a preview of the waiting message, shown only while the panel is closed */
.hw-teaser{display:flex;gap:10px;width:min(290px,100%);padding:14px 16px;border:1px solid var(--hw-line);border-radius:18px 18px 6px 18px;background:var(--hw-bg);box-shadow:0 18px 40px -20px rgba(45,24,99,.45);font-size:.88rem}
.hw-teaser .hw-av{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:#bae6fd;color:#0c4a6e;font-size:.7rem;font-weight:700}
.hw-teaser b{display:block;font-size:.78rem;color:var(--hw-muted)}
.hw-panel:not([hidden])+.hw-teaser{display:none}

/* launcher */
.hw-launcher{position:relative;display:flex;align-items:center;gap:10px;height:60px;min-width:60px;padding:0 18px;border:0;border-radius:999px;
  background:var(--hw-accent);color:#fff;font-weight:700;box-shadow:0 14px 30px -10px rgba(76,29,149,.6);transition:transform .15s,background-color .15s}
.hw-launcher:hover{background:var(--hw-accent-dark);transform:translateY(-2px)}
.hw-launcher:focus-visible{outline-offset:3px}
.hw-launcher .ic-close{display:none}
.hw-launcher[aria-expanded="true"] .ic-open{display:none}
.hw-launcher[aria-expanded="true"] .ic-close{display:block}
.hw-badge{position:absolute;top:-4px;right:-4px;display:grid;place-items:center;min-width:22px;height:22px;padding:0 6px;border:2px solid #fff;border-radius:999px;background:#be123c;color:#fff;font-size:.72rem}
@media (prefers-reduced-motion:reduce){
  .hw-panel,.hw-dots i{animation:none}
  .hw-dots i{opacity:.8}
  .hw-launcher{transition:none}
  .hw-launcher:hover{transform:none}
}
JavaScript
/* Support widget: the launcher toggles the panel, Esc or the close button shut it and return focus to the launcher.
   Sending appends your message, shows a typing indicator, then a canned reply. Nothing leaves the page. */
document.querySelectorAll('[data-widget]').forEach(function (w) {
  var panel = w.querySelector('[data-panel]'), launcher = w.querySelector('[data-launcher]');
  var log = w.querySelector('[data-log]'), form = w.querySelector('[data-composer]'), input = w.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]'), typing = w.querySelector('[data-typing]'), quick = w.querySelector('[data-quick]');
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
  var pad = function (n) { return String(n).padStart(2, '0'); };

  function setOpen(open) {
    panel.hidden = !open;
    launcher.setAttribute('aria-expanded', String(open));
    if (open) {
      var badge = launcher.querySelector('[data-badge]');
      if (badge) badge.remove();
      log.scrollTop = log.scrollHeight;
      input.focus();
    } else launcher.focus();
  }
  launcher.addEventListener('click', function () { setOpen(panel.hidden); });
  w.querySelector('[data-close]').addEventListener('click', function () { setOpen(false); });
  w.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !panel.hidden) { e.preventDefault(); setOpen(false); } });

  function add(kind, text) {
    var d = new Date(), msg = w.querySelector('template[data-tpl="' + kind + '"]').content.cloneNode(true), time = msg.querySelector('[data-time]');
    msg.querySelector('[data-text]').textContent = text;
    time.textContent = pad(d.getHours()) + ':' + pad(d.getMinutes());
    time.dateTime = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + time.textContent;
    log.appendChild(msg);
    log.scrollTop = log.scrollHeight;
  }
  function reply(text) {
    if (/order|track|deliver/i.test(text)) return 'Order LM-20418 left our warehouse on Monday and should reach you on Thursday.';
    if (/return|refund/i.test(text)) return 'You can return any lamp within 60 days. I have emailed you a prepaid label.';
    if (/person|human|someone/i.test(text)) return 'Of course. Oskar from the support team will join this chat in a moment.';
    return 'Thanks. A teammate will answer here shortly, and you can keep browsing in the meantime.';
  }
  function ask(text) {
    add('out', text);
    if (quick) quick.hidden = true;
    typing.hidden = false;
    setTimeout(function () { typing.hidden = true; add('in', reply(text)); }, reduce.matches ? 300 : 1400);
  }
  function grow() { input.style.height = 'auto'; input.style.height = input.scrollHeight + 2 + 'px'; send.disabled = !input.value.trim(); }
  input.addEventListener('input', grow);
  input.addEventListener('keydown', function (e) {
    if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); form.requestSubmit(); }
  });
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var text = input.value.trim();
    if (!text) return;
    ask(text); input.value = ''; grow(); input.focus();
  });
  if (quick) quick.querySelectorAll('button').forEach(function (b) {
    b.addEventListener('click', function () { ask(b.textContent); input.focus(); });
  });
  grow();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Help widget with team intro and quick replies</span>
<div class="stage"><div class="page" aria-hidden="true"><i></i><i></i><i></i><i></i><b></b><i></i><i></i></div>
<div class="hw" data-widget>
  <section class="card hw-panel" id="hw-panel-a" aria-labelledby="hw-title-a" data-panel>
    <div class="card-header hw-head">
      <div class="hw-team d-flex mb-2" aria-hidden="true"><span>PS</span><span>OK</span><span>LB</span></div>
      <h2 class="fw-bold" id="hw-title-a">Lumen Support</h2>
      <p>Usually replies in under 5 minutes</p>
      <button class="btn-close btn-close-white hw-close" type="button" aria-label="Close chat" data-close></button>
    </div>
    <div class="hw-log flex-grow-1" role="log" aria-live="polite" aria-labelledby="hw-title-a" tabindex="0" data-log>
      <div class="hw-msg"><p class="hw-who">Priya from Lumen</p><p class="hw-b"><span>Hi, I'm Priya. Ask me about an order, a return, or a lamp that won't pair with the app.</span> <time datetime="2026-10-14T10:30">10:30</time></p></div>
    </div>
    <div class="hw-typing d-flex align-items-center gap-2 px-3 pb-2" data-typing hidden><span class="hw-dots" aria-hidden="true"><i></i><i></i><i></i></span><span>Priya is typing</span></div>
    <div class="hw-quick d-flex flex-wrap px-3 pb-2" role="group" aria-label="Suggested questions" data-quick>
      <button class="btn btn-outline-secondary rounded-pill hw-chip" type="button">Where is my order?</button><button class="btn btn-outline-secondary rounded-pill hw-chip" type="button">Start a return</button><button class="btn btn-outline-secondary rounded-pill hw-chip" type="button">Talk to a person</button>
    </div>
    <form class="card-footer hw-composer d-flex align-items-end gap-2" data-composer>
      <label class="visually-hidden" for="hw-input-a">Message to Lumen Support</label>
      <textarea class="form-control hw-input" id="hw-input-a" rows="1" placeholder="Type your question" data-input></textarea>
      <button class="btn hw-send" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg><span class="visually-hidden">Send</span></button>
    </form>
    <template data-tpl="out"><div class="hw-msg out"><p class="hw-who visually-hidden">You</p><p class="hw-b"><span data-text></span> <time data-time></time></p></div></template>
    <template data-tpl="in"><div class="hw-msg"><p class="hw-who">Priya from Lumen</p><p class="hw-b"><span data-text></span> <time data-time></time></p></div></template>
  </section>
  <button class="btn hw-launcher" type="button" aria-expanded="true" aria-controls="hw-panel-a" data-launcher>
    <svg class="ic-open" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20.5l1.4-4.9A8 8 0 1 1 21 12z"/></svg>
    <svg class="ic-close" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
    <span class="visually-hidden">Support chat</span>
  </button>
</div></div></div>

<div><span class="lbl">Labelled launcher with an unread message</span>
<div class="stage"><div class="page" aria-hidden="true"><i></i><i></i><i></i><i></i><b></b><i></i><i></i></div>
<div class="hw" data-widget>
  <section class="card hw-panel short" id="hw-panel-b" aria-labelledby="hw-title-b" data-panel hidden>
    <div class="card-header hw-head">
      <div class="hw-team d-flex mb-2" aria-hidden="true"><span>OK</span></div>
      <h2 class="fw-bold" id="hw-title-b">Oskar from Lumen</h2>
      <p>Online now</p>
      <button class="btn-close btn-close-white hw-close" type="button" aria-label="Close chat" data-close></button>
    </div>
    <div class="hw-log flex-grow-1" role="log" aria-live="polite" aria-labelledby="hw-title-b" tabindex="0" data-log>
      <div class="hw-msg"><p class="hw-who">Oskar from Lumen</p><p class="hw-b"><span>The Halo desk lamp comes in two heights, 38 and 52 cm. Happy to help you pick one.</span> <time datetime="2026-10-14T10:41">10:41</time></p></div>
    </div>
    <div class="hw-typing d-flex align-items-center gap-2 px-3 pb-2" data-typing hidden><span class="hw-dots" aria-hidden="true"><i></i><i></i><i></i></span><span>Oskar is typing</span></div>
    <form class="card-footer hw-composer d-flex align-items-end gap-2" data-composer>
      <label class="visually-hidden" for="hw-input-b">Message to Oskar</label>
      <textarea class="form-control hw-input" id="hw-input-b" rows="1" placeholder="Type your question" data-input></textarea>
      <button class="btn hw-send" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg><span class="visually-hidden">Send</span></button>
    </form>
    <template data-tpl="out"><div class="hw-msg out"><p class="hw-who visually-hidden">You</p><p class="hw-b"><span data-text></span> <time data-time></time></p></div></template>
    <template data-tpl="in"><div class="hw-msg"><p class="hw-who">Oskar from Lumen</p><p class="hw-b"><span data-text></span> <time data-time></time></p></div></template>
  </section>
  <p class="card hw-teaser flex-row gap-2 px-3 py-3"><span class="hw-av flex-shrink-0" aria-hidden="true">OK</span><span><b class="d-block">Oskar from Lumen</b>The Halo desk lamp comes in two heights. Happy to help you pick one.</span></p>
  <button class="btn hw-launcher" type="button" aria-expanded="false" aria-controls="hw-panel-b" data-launcher>
    <svg class="ic-open" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20.5l1.4-4.9A8 8 0 1 1 21 12z"/></svg>
    <svg class="ic-close" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
    <span>Questions? Chat with us</span>
    <span class="badge rounded-pill hw-badge position-absolute top-0 end-0 translate-middle-y" data-badge>1<span class="visually-hidden"> new message</span></span>
  </button>
</div></div></div>
CSS
/* Support chat widget on Bootstrap: the panel is a .card, the close button is .btn-close, quick replies are .btn, the composer uses .form-control.
   A launcher toggles the panel (aria-expanded); Esc closes it. The dock is position:absolute in the demo; use position:fixed on a real page. */
.hw{--hw-accent:#6d28d9;--hw-accent-dark:#5b21b6;--hw-soft:#ede6ff;--hw-in:#f2f0f7;--hw-ink:#1d1830;--hw-muted:#5c5670;--hw-line:#e3def2;
  position:absolute;right:20px;bottom:20px;left:20px;display:flex;flex-direction:column;align-items:flex-end;gap:14px;color:var(--hw-ink);font-size:.93rem;line-height:1.45;pointer-events:none}
.hw>*{pointer-events:auto}
.hw :where(h2,p){margin:0}
.hw :focus-visible{outline:2px solid var(--hw-accent);outline-offset:2px;box-shadow:none}
.hw-panel{--bs-card-border-color:var(--hw-line);--bs-card-border-radius:20px;--bs-card-inner-border-radius:19px;--bs-card-cap-padding-y:18px;--bs-card-cap-padding-x:20px;
  width:min(370px,100%);height:500px;overflow:hidden;box-shadow:0 28px 60px -24px rgba(45,24,99,.45);animation:hw-in .22s ease-out}
.hw-panel[hidden]{display:none}
.hw-panel.short{height:400px}
@keyframes hw-in{from{opacity:0;transform:translateY(10px) scale(.98)}}
.hw-head{position:relative;padding-right:56px!important;border:0;background:linear-gradient(135deg,#6d28d9,#4c1d95);color:#fff}
.hw-team span{display:grid;place-items:center;width:34px;height:34px;margin-right:-8px;border:2px solid #5b21b6;border-radius:50%;background:#f5d0fe;color:#5b1470;font-size:.7rem;font-weight:700}
.hw-team span:nth-child(2){background:#bae6fd;color:#0c4a6e}
.hw-team span:nth-child(3){background:#fde68a;color:#713f12}
.hw-head h2{font-size:1.05rem}
.hw-head p{margin-top:2px;font-size:.82rem;color:#ece6ff}
.hw-close{--bs-btn-close-opacity:1;--bs-btn-close-hover-opacity:1;--bs-btn-close-focus-shadow:none;position:absolute;top:14px;right:14px;width:34px;height:34px;padding:0;border-radius:50%;background-color:rgba(255,255,255,.14);background-size:12px}
.hw-close:hover{background-color:rgba(255,255,255,.26)}
.hw-head :focus-visible{outline-color:#fff}
.hw-log{display:flex;flex-direction:column;gap:12px;overflow-y:auto;padding:18px 16px}
.hw-log:focus-visible{outline-offset:-3px}
.hw-msg{display:flex;flex-direction:column;align-items:flex-start;gap:3px;max-width:86%}
.hw-msg.out{align-self:flex-end;align-items:flex-end}
.hw-who{margin-left:12px;font-size:.75rem;font-weight:600;color:var(--hw-muted)}
.hw-b{display:flex;flex-wrap:wrap;align-items:flex-end;column-gap:10px;padding:8px 13px 7px;border-radius:6px 18px 18px 18px;background:var(--hw-in);overflow-wrap:anywhere;white-space:pre-wrap}
.hw-msg.out .hw-b{border-radius:18px 6px 18px 18px;background:var(--hw-accent);color:#fff}
.hw-b time{margin-left:auto;font-size:.7rem;color:var(--hw-muted);white-space:nowrap}
.hw-msg.out .hw-b time{color:#e4d9ff}
.hw-typing{font-size:.78rem;color:var(--hw-muted)}
.hw-typing[hidden]{display:none!important}
.hw-dots{display:flex;gap:3px;padding:8px 10px;border-radius:12px;background:var(--hw-in)}
.hw-dots i{width:6px;height:6px;border-radius:50%;background:var(--hw-muted);animation:hw-dot 1s infinite ease-in-out}
.hw-dots i:nth-child(2){animation-delay:.15s}.hw-dots i:nth-child(3){animation-delay:.3s}
@keyframes hw-dot{0%,60%,100%{transform:none;opacity:.5}30%{transform:translateY(-3px);opacity:1}}
.hw-quick{gap:6px}
.hw-quick[hidden]{display:none!important}
.hw-chip{--bs-btn-padding-x:12px;--bs-btn-padding-y:5px;--bs-btn-line-height:1.45;--bs-btn-font-size:.82rem;--bs-btn-font-weight:600;--bs-btn-color:var(--hw-accent-dark);--bs-btn-border-color:#a78bfa;--bs-btn-border-width:1.5px;
  --bs-btn-hover-color:var(--hw-accent-dark);--bs-btn-hover-bg:var(--hw-soft);--bs-btn-hover-border-color:#a78bfa;--bs-btn-active-color:var(--hw-accent-dark);--bs-btn-active-bg:var(--hw-soft);--bs-btn-active-border-color:#a78bfa}
.hw-composer{--bs-card-cap-padding-y:10px;--bs-card-cap-padding-x:12px;background:#fff;border-top-color:var(--hw-line)}
.hw-input{--bs-border-color:#8f88a3;max-height:110px;padding:10px 14px;border-width:1.5px;border-radius:16px;color:var(--hw-ink);font-size:inherit;line-height:1.4;resize:none}
.hw-input::placeholder{color:#686279}
.hw-input:focus{border-color:var(--hw-accent);box-shadow:none}
.hw-input:focus-visible{outline-offset:1px}
.hw-send{--bs-btn-padding-x:0;--bs-btn-padding-y:0;--bs-btn-border-radius:14px;--bs-btn-color:#fff;--bs-btn-bg:var(--hw-accent);--bs-btn-border-color:var(--hw-accent);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--hw-accent-dark);--bs-btn-hover-border-color:var(--hw-accent-dark);
  --bs-btn-active-color:#fff;--bs-btn-active-bg:var(--hw-accent-dark);--bs-btn-disabled-color:#4f4963;--bs-btn-disabled-bg:#d9d4e6;--bs-btn-disabled-border-color:#d9d4e6;--bs-btn-disabled-opacity:1;display:grid;place-items:center;flex:none;width:42px;height:42px}
.hw-teaser{--bs-card-border-color:var(--hw-line);--bs-card-border-radius:18px;width:min(290px,100%);border-bottom-right-radius:6px;box-shadow:0 18px 40px -20px rgba(45,24,99,.45);font-size:.88rem}
.hw-teaser .hw-av{display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:#bae6fd;color:#0c4a6e;font-size:.7rem;font-weight:700}
.hw-teaser b{font-size:.78rem;color:var(--hw-muted)}
.hw-panel:not([hidden])+.hw-teaser{display:none!important}
.hw-launcher{--bs-btn-padding-x:18px;--bs-btn-padding-y:0;--bs-btn-font-size:.93rem;--bs-btn-font-weight:700;--bs-btn-color:#fff;--bs-btn-bg:var(--hw-accent);--bs-btn-border-color:var(--hw-accent);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--hw-accent-dark);--bs-btn-hover-border-color:var(--hw-accent-dark);
  --bs-btn-active-color:#fff;--bs-btn-active-bg:var(--hw-accent-dark);--bs-btn-active-border-color:var(--hw-accent-dark);--bs-btn-border-radius:999px;
  position:relative;display:flex;align-items:center;gap:10px;height:60px;min-width:60px;box-shadow:0 14px 30px -10px rgba(76,29,149,.6);transition:transform .15s,background-color .15s}
.hw-launcher:hover{transform:translateY(-2px)}
.hw-launcher:focus-visible{outline-offset:3px}
.hw-launcher .ic-close{display:none}
.hw-launcher[aria-expanded="true"] .ic-open{display:none}
.hw-launcher[aria-expanded="true"] .ic-close{display:block}
.hw-badge{--bs-badge-font-size:.72rem;--bs-badge-padding-x:6px;--bs-badge-padding-y:3px;min-width:22px;border:2px solid #fff;background:#be123c}
@media (prefers-reduced-motion:reduce){
  .hw-panel,.hw-dots i{animation:none}
  .hw-dots i{opacity:.8}
  .hw-launcher{transition:none}
  .hw-launcher:hover{transform:none}
}
JavaScript
/* Support widget: the launcher toggles the panel, Esc or the close button shut it and return focus to the launcher.
   Sending appends your message, shows a typing indicator, then a canned reply. Nothing leaves the page. */
document.querySelectorAll('[data-widget]').forEach(function (w) {
  var panel = w.querySelector('[data-panel]'), launcher = w.querySelector('[data-launcher]');
  var log = w.querySelector('[data-log]'), form = w.querySelector('[data-composer]'), input = w.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]'), typing = w.querySelector('[data-typing]'), quick = w.querySelector('[data-quick]');
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
  var pad = function (n) { return String(n).padStart(2, '0'); };

  function setOpen(open) {
    panel.hidden = !open;
    launcher.setAttribute('aria-expanded', String(open));
    if (open) {
      var badge = launcher.querySelector('[data-badge]');
      if (badge) badge.remove();
      log.scrollTop = log.scrollHeight;
      input.focus();
    } else launcher.focus();
  }
  launcher.addEventListener('click', function () { setOpen(panel.hidden); });
  w.querySelector('[data-close]').addEventListener('click', function () { setOpen(false); });
  w.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !panel.hidden) { e.preventDefault(); setOpen(false); } });

  function add(kind, text) {
    var d = new Date(), msg = w.querySelector('template[data-tpl="' + kind + '"]').content.cloneNode(true), time = msg.querySelector('[data-time]');
    msg.querySelector('[data-text]').textContent = text;
    time.textContent = pad(d.getHours()) + ':' + pad(d.getMinutes());
    time.dateTime = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + time.textContent;
    log.appendChild(msg);
    log.scrollTop = log.scrollHeight;
  }
  function reply(text) {
    if (/order|track|deliver/i.test(text)) return 'Order LM-20418 left our warehouse on Monday and should reach you on Thursday.';
    if (/return|refund/i.test(text)) return 'You can return any lamp within 60 days. I have emailed you a prepaid label.';
    if (/person|human|someone/i.test(text)) return 'Of course. Oskar from the support team will join this chat in a moment.';
    return 'Thanks. A teammate will answer here shortly, and you can keep browsing in the meantime.';
  }
  function ask(text) {
    add('out', text);
    if (quick) quick.hidden = true;
    typing.hidden = false;
    setTimeout(function () { typing.hidden = true; add('in', reply(text)); }, reduce.matches ? 300 : 1400);
  }
  function grow() { input.style.height = 'auto'; input.style.height = input.scrollHeight + 2 + 'px'; send.disabled = !input.value.trim(); }
  input.addEventListener('input', grow);
  input.addEventListener('keydown', function (e) {
    if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); form.requestSubmit(); }
  });
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var text = input.value.trim();
    if (!text) return;
    ask(text); input.value = ''; grow(); input.focus();
  });
  if (quick) quick.querySelectorAll('button').forEach(function (b) {
    b.addEventListener('click', function () { ask(b.textContent); input.focus(); });
  });
  grow();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-hw-muted uppercase">Help widget with team intro and quick replies</span>
<div class="relative h-[660px] overflow-hidden rounded-[18px] border border-hw-line bg-white"><div class="page" aria-hidden="true"><i></i><i></i><i></i><i></i><b></b><i></i><i></i></div>
<div class="pointer-events-none absolute inset-x-5 bottom-5 flex flex-col items-end gap-3.5 text-[.93rem] leading-[1.45] text-hw-ink" data-widget>
  <section class="pointer-events-auto flex h-[500px] w-[min(370px,100%)] animate-hw-in flex-col overflow-hidden rounded-[20px] border border-hw-line bg-white shadow-[0_28px_60px_-24px_rgba(45,24,99,.45)] motion-reduce:animate-none" id="hw-panel-a" aria-labelledby="hw-title-a" data-panel>
    <div class="relative bg-linear-135 from-[#6d28d9] to-[#4c1d95] py-[18px] pr-14 pl-5 text-white">
      <div class="mb-2.5 flex" aria-hidden="true"><span class="-mr-2 grid size-[34px] place-items-center rounded-full border-2 border-hw-accent-dark bg-[#f5d0fe] text-[.7rem] font-bold text-[#5b1470]">PS</span><span class="-mr-2 grid size-[34px] place-items-center rounded-full border-2 border-hw-accent-dark bg-[#bae6fd] text-[.7rem] font-bold text-[#0c4a6e]">OK</span><span class="-mr-2 grid size-[34px] place-items-center rounded-full border-2 border-hw-accent-dark bg-[#fde68a] text-[.7rem] font-bold text-[#713f12]">LB</span></div>
      <h2 class="text-[1.05rem] font-bold" id="hw-title-a">Lumen Support</h2>
      <p class="mt-0.5 text-[.82rem] text-[#ece6ff]">Usually replies in under 5 minutes</p>
      <button class="absolute top-3.5 right-3.5 grid size-[34px] cursor-pointer place-items-center rounded-full bg-white/14 text-white hover:bg-white/26 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" type="button" data-close><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg><span class="sr-only">Close chat</span></button>
    </div>
    <div class="flex flex-1 flex-col gap-3 overflow-y-auto px-4 py-[18px] focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-hw-accent" role="log" aria-live="polite" aria-labelledby="hw-title-a" tabindex="0" data-log>
      <div class="flex max-w-[86%] flex-col items-start gap-[3px]"><p class="ml-3 text-[.75rem] font-semibold text-hw-muted">Priya from Lumen</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_18px] bg-hw-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>Hi, I'm Priya. Ask me about an order, a return, or a lamp that won't pair with the app.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-hw-muted" datetime="2026-10-14T10:30">10:30</time></p></div>
    </div>
    <div class="flex items-center gap-2 px-4 pb-2.5 text-[.78rem] text-hw-muted" data-typing hidden><span class="flex gap-[3px] rounded-xl bg-hw-in px-2.5 py-2" aria-hidden="true"><i class="size-1.5 animate-hw-dot rounded-full bg-hw-muted motion-reduce:animate-none motion-reduce:opacity-80"></i><i class="size-1.5 animate-hw-dot rounded-full bg-hw-muted [animation-delay:.15s] motion-reduce:animate-none motion-reduce:opacity-80"></i><i class="size-1.5 animate-hw-dot rounded-full bg-hw-muted [animation-delay:.3s] motion-reduce:animate-none motion-reduce:opacity-80"></i></span><span>Priya is typing</span></div>
    <div class="flex flex-wrap gap-1.5 px-4 pb-3" role="group" aria-label="Suggested questions" data-quick>
      <button class="cursor-pointer rounded-full border-[1.5px] border-[#a78bfa] bg-white px-3 py-1.5 text-[.82rem] font-semibold text-hw-accent-dark hover:bg-hw-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hw-accent" type="button">Where is my order?</button><button class="cursor-pointer rounded-full border-[1.5px] border-[#a78bfa] bg-white px-3 py-1.5 text-[.82rem] font-semibold text-hw-accent-dark hover:bg-hw-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hw-accent" type="button">Start a return</button><button class="cursor-pointer rounded-full border-[1.5px] border-[#a78bfa] bg-white px-3 py-1.5 text-[.82rem] font-semibold text-hw-accent-dark hover:bg-hw-soft focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hw-accent" type="button">Talk to a person</button>
    </div>
    <form class="flex items-end gap-2 border-t border-hw-line px-3 pt-2.5 pb-3" data-composer>
      <label class="sr-only" for="hw-input-a">Message to Lumen Support</label>
      <textarea class="max-h-[110px] min-w-0 flex-1 resize-none rounded-2xl border-[1.5px] border-[#8f88a3] bg-white px-3.5 py-2.5 leading-[1.4] text-hw-ink placeholder:text-[#686279] focus-visible:border-hw-accent focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-hw-accent" id="hw-input-a" rows="1" placeholder="Type your question" data-input></textarea>
      <button class="grid size-[42px] flex-none cursor-pointer place-items-center rounded-[14px] bg-hw-accent text-white hover:bg-hw-accent-dark focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hw-accent disabled:cursor-not-allowed disabled:bg-[#d9d4e6] disabled:text-[#4f4963]" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg><span class="sr-only">Send</span></button>
    </form>
    <template data-tpl="out"><div class="flex max-w-[86%] flex-col items-end gap-[3px] self-end"><p class="sr-only">You</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_6px_18px_18px] bg-hw-accent px-[13px] pt-2 pb-[7px] whitespace-pre-wrap text-white [overflow-wrap:anywhere]"><span data-text></span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-[#e4d9ff]" data-time></time></p></div></template>
    <template data-tpl="in"><div class="flex max-w-[86%] flex-col items-start gap-[3px]"><p class="ml-3 text-[.75rem] font-semibold text-hw-muted">Priya from Lumen</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_18px] bg-hw-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span data-text></span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-hw-muted" data-time></time></p></div></template>
  </section>
  <button class="group pointer-events-auto relative flex h-[60px] min-w-[60px] cursor-pointer items-center justify-center gap-2.5 rounded-full bg-hw-accent px-[18px] font-bold text-white shadow-[0_14px_30px_-10px_rgba(76,29,149,.6)] transition-[transform,background-color] duration-150 hover:-translate-y-0.5 hover:bg-hw-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-hw-accent motion-reduce:transition-none motion-reduce:hover:translate-y-0" type="button" aria-expanded="true" aria-controls="hw-panel-a" data-launcher>
    <svg class="group-aria-expanded:hidden" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20.5l1.4-4.9A8 8 0 1 1 21 12z"/></svg>
    <svg class="hidden group-aria-expanded:block" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
    <span class="sr-only">Support chat</span>
  </button>
</div></div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-hw-muted uppercase">Labelled launcher with an unread message</span>
<div class="relative h-[660px] overflow-hidden rounded-[18px] border border-hw-line bg-white"><div class="page" aria-hidden="true"><i></i><i></i><i></i><i></i><b></b><i></i><i></i></div>
<div class="pointer-events-none absolute inset-x-5 bottom-5 flex flex-col items-end gap-3.5 text-[.93rem] leading-[1.45] text-hw-ink" data-widget>
  <section class="pointer-events-auto flex h-[400px] w-[min(370px,100%)] animate-hw-in flex-col overflow-hidden rounded-[20px] border border-hw-line bg-white shadow-[0_28px_60px_-24px_rgba(45,24,99,.45)] motion-reduce:animate-none" id="hw-panel-b" aria-labelledby="hw-title-b" data-panel hidden>
    <div class="relative bg-linear-135 from-[#6d28d9] to-[#4c1d95] py-[18px] pr-14 pl-5 text-white">
      <div class="mb-2.5 flex" aria-hidden="true"><span class="-mr-2 grid size-[34px] place-items-center rounded-full border-2 border-hw-accent-dark bg-[#bae6fd] text-[.7rem] font-bold text-[#0c4a6e]">OK</span></div>
      <h2 class="text-[1.05rem] font-bold" id="hw-title-b">Oskar from Lumen</h2>
      <p class="mt-0.5 text-[.82rem] text-[#ece6ff]">Online now</p>
      <button class="absolute top-3.5 right-3.5 grid size-[34px] cursor-pointer place-items-center rounded-full bg-white/14 text-white hover:bg-white/26 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white" type="button" data-close><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg><span class="sr-only">Close chat</span></button>
    </div>
    <div class="flex flex-1 flex-col gap-3 overflow-y-auto px-4 py-[18px] focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-hw-accent" role="log" aria-live="polite" aria-labelledby="hw-title-b" tabindex="0" data-log>
      <div class="flex max-w-[86%] flex-col items-start gap-[3px]"><p class="ml-3 text-[.75rem] font-semibold text-hw-muted">Oskar from Lumen</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_18px] bg-hw-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>The Halo desk lamp comes in two heights, 38 and 52 cm. Happy to help you pick one.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-hw-muted" datetime="2026-10-14T10:41">10:41</time></p></div>
    </div>
    <div class="flex items-center gap-2 px-4 pb-2.5 text-[.78rem] text-hw-muted" data-typing hidden><span class="flex gap-[3px] rounded-xl bg-hw-in px-2.5 py-2" aria-hidden="true"><i class="size-1.5 animate-hw-dot rounded-full bg-hw-muted motion-reduce:animate-none motion-reduce:opacity-80"></i><i class="size-1.5 animate-hw-dot rounded-full bg-hw-muted [animation-delay:.15s] motion-reduce:animate-none motion-reduce:opacity-80"></i><i class="size-1.5 animate-hw-dot rounded-full bg-hw-muted [animation-delay:.3s] motion-reduce:animate-none motion-reduce:opacity-80"></i></span><span>Oskar is typing</span></div>
    <form class="flex items-end gap-2 border-t border-hw-line px-3 pt-2.5 pb-3" data-composer>
      <label class="sr-only" for="hw-input-b">Message to Oskar</label>
      <textarea class="max-h-[110px] min-w-0 flex-1 resize-none rounded-2xl border-[1.5px] border-[#8f88a3] bg-white px-3.5 py-2.5 leading-[1.4] text-hw-ink placeholder:text-[#686279] focus-visible:border-hw-accent focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-hw-accent" id="hw-input-b" rows="1" placeholder="Type your question" data-input></textarea>
      <button class="grid size-[42px] flex-none cursor-pointer place-items-center rounded-[14px] bg-hw-accent text-white hover:bg-hw-accent-dark focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-hw-accent disabled:cursor-not-allowed disabled:bg-[#d9d4e6] disabled:text-[#4f4963]" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg><span class="sr-only">Send</span></button>
    </form>
    <template data-tpl="out"><div class="flex max-w-[86%] flex-col items-end gap-[3px] self-end"><p class="sr-only">You</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[18px_6px_18px_18px] bg-hw-accent px-[13px] pt-2 pb-[7px] whitespace-pre-wrap text-white [overflow-wrap:anywhere]"><span data-text></span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-[#e4d9ff]" data-time></time></p></div></template>
    <template data-tpl="in"><div class="flex max-w-[86%] flex-col items-start gap-[3px]"><p class="ml-3 text-[.75rem] font-semibold text-hw-muted">Oskar from Lumen</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_18px] bg-hw-in px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span data-text></span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-hw-muted" data-time></time></p></div></template>
  </section>
  <p class="pointer-events-auto flex w-[min(290px,100%)] gap-2.5 rounded-[18px_18px_6px_18px] border border-hw-line bg-white px-4 py-3.5 text-[.88rem] shadow-[0_18px_40px_-20px_rgba(45,24,99,.45)] [[data-panel]:not([hidden])+&]:hidden"><span class="grid size-[34px] flex-none place-items-center rounded-full bg-[#bae6fd] text-[.7rem] font-bold text-[#0c4a6e]" aria-hidden="true">OK</span><span><b class="block text-[.78rem] text-hw-muted">Oskar from Lumen</b>The Halo desk lamp comes in two heights. Happy to help you pick one.</span></p>
  <button class="group pointer-events-auto relative flex h-[60px] min-w-[60px] cursor-pointer items-center justify-center gap-2.5 rounded-full bg-hw-accent px-[18px] font-bold text-white shadow-[0_14px_30px_-10px_rgba(76,29,149,.6)] transition-[transform,background-color] duration-150 hover:-translate-y-0.5 hover:bg-hw-accent-dark focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-hw-accent motion-reduce:transition-none motion-reduce:hover:translate-y-0" type="button" aria-expanded="false" aria-controls="hw-panel-b" data-launcher>
    <svg class="group-aria-expanded:hidden" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20.5l1.4-4.9A8 8 0 1 1 21 12z"/></svg>
    <svg class="hidden group-aria-expanded:block" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>
    <span>Questions? Chat with us</span>
    <span class="absolute -top-1 -right-1 grid h-[22px] min-w-[22px] place-items-center rounded-full border-2 border-white bg-[#be123c] px-1.5 text-[.72rem] text-white" data-badge>1<span class="sr-only"> new message</span></span>
  </button>
</div></div></div>
CSS
/* Widget palette plus the panel entrance and typing-dot keyframes (both switched off with motion-reduce:). */
@theme {
  --color-hw-accent:#6d28d9; --color-hw-accent-dark:#5b21b6; --color-hw-soft:#ede6ff; --color-hw-in:#f2f0f7;
  --color-hw-ink:#1d1830; --color-hw-muted:#5c5670; --color-hw-line:#e3def2;
  --animate-hw-in: hw-in .22s ease-out;
  --animate-hw-dot: hw-dot 1s infinite ease-in-out;
  @keyframes hw-in { from { opacity:0; transform:translateY(10px) scale(.98) } }
  @keyframes hw-dot { 0%,60%,100% { transform:none; opacity:.5 } 30% { transform:translateY(-3px); opacity:1 } }
}
JavaScript
/* Support widget: the launcher toggles the panel, Esc or the close button shut it and return focus to the launcher.
   Sending appends your message, shows a typing indicator, then a canned reply. Nothing leaves the page. */
document.querySelectorAll('[data-widget]').forEach(function (w) {
  var panel = w.querySelector('[data-panel]'), launcher = w.querySelector('[data-launcher]');
  var log = w.querySelector('[data-log]'), form = w.querySelector('[data-composer]'), input = w.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]'), typing = w.querySelector('[data-typing]'), quick = w.querySelector('[data-quick]');
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
  var pad = function (n) { return String(n).padStart(2, '0'); };

  function setOpen(open) {
    panel.hidden = !open;
    launcher.setAttribute('aria-expanded', String(open));
    if (open) {
      var badge = launcher.querySelector('[data-badge]');
      if (badge) badge.remove();
      log.scrollTop = log.scrollHeight;
      input.focus();
    } else launcher.focus();
  }
  launcher.addEventListener('click', function () { setOpen(panel.hidden); });
  w.querySelector('[data-close]').addEventListener('click', function () { setOpen(false); });
  w.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !panel.hidden) { e.preventDefault(); setOpen(false); } });

  function add(kind, text) {
    var d = new Date(), msg = w.querySelector('template[data-tpl="' + kind + '"]').content.cloneNode(true), time = msg.querySelector('[data-time]');
    msg.querySelector('[data-text]').textContent = text;
    time.textContent = pad(d.getHours()) + ':' + pad(d.getMinutes());
    time.dateTime = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + time.textContent;
    log.appendChild(msg);
    log.scrollTop = log.scrollHeight;
  }
  function reply(text) {
    if (/order|track|deliver/i.test(text)) return 'Order LM-20418 left our warehouse on Monday and should reach you on Thursday.';
    if (/return|refund/i.test(text)) return 'You can return any lamp within 60 days. I have emailed you a prepaid label.';
    if (/person|human|someone/i.test(text)) return 'Of course. Oskar from the support team will join this chat in a moment.';
    return 'Thanks. A teammate will answer here shortly, and you can keep browsing in the meantime.';
  }
  function ask(text) {
    add('out', text);
    if (quick) quick.hidden = true;
    typing.hidden = false;
    setTimeout(function () { typing.hidden = true; add('in', reply(text)); }, reduce.matches ? 300 : 1400);
  }
  function grow() { input.style.height = 'auto'; input.style.height = input.scrollHeight + 2 + 'px'; send.disabled = !input.value.trim(); }
  input.addEventListener('input', grow);
  input.addEventListener('keydown', function (e) {
    if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); form.requestSubmit(); }
  });
  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var text = input.value.trim();
    if (!text) return;
    ask(text); input.value = ''; grow(); input.focus();
  });
  if (quick) quick.querySelectorAll('button').forEach(function (b) {
    b.addEventListener('click', function () { ask(b.textContent); input.focus(); });
  });
  grow();
});

About this set

A floating support chat for any website, shown in two forms. The help widget opens on a panel with a gradient header, a stack of team initials, a reply-time promise, a welcome message and three quick-reply chips such as Where is my order?. The labelled launcher is the closed state: a pill that reads Questions? Chat with us, a red badge for one unread message and a teaser card that previews that message. The launcher is a button with aria-expanded and aria-controls; opening the panel moves focus into the message field and removes the badge, while Esc or the close button shuts it and returns focus to the launcher. Sending, or picking a quick reply, appends your message, shows a typing indicator and then a canned reply chosen by keyword, so the demo feels live without a server. The dock is absolutely positioned inside the demo stage; switch it to position: fixed to pin it to the corner of a real page. The Bootstrap version builds the panel from .card with .btn-close, .btn chips and .form-control.

What’s included

  • Launcher button with aria-expanded and an unread badge
  • Esc and the close button return focus to the launcher
  • Quick-reply chips and a teaser card for the closed state
  • Typing indicator that holds still under reduced motion
  • Canned demo replies; nothing leaves the page

Accessibility

The launcher is a real button with aria-expanded and aria-controls pointing at the panel, and its unread badge carries hidden text. Opening moves focus to the labelled textarea, Esc anywhere in the widget closes it and focus returns to the launcher. The message list is a role="log" region with aria-live="polite", the typing indicator is written as text, and the panel entrance and typing dots are switched off under prefers-reduced-motion.

Customise it

Change --hw-accent, --hw-accent-dark and --hw-in on .hw, and the right and bottom offsets of .hw to move the dock. In Tailwind edit the --color-hw-* theme colours and the inset-x-5 and bottom-5 utilities; in Bootstrap the same --hw-* variables sit on the dock.