Open demo

CSS code

HTML
<div><span class="lbl">Direct messages with gradient bubbles</span>
<div class="dk" data-chat>
  <div class="dk-head">
    <span class="dk-av" aria-hidden="true">JR<i></i></span>
    <div><h2 id="dk-title-a">Juno Reyes</h2><p>Online</p></div>
  </div>
  <div class="dk-log" role="log" aria-live="polite" aria-labelledby="dk-title-a" tabindex="0" data-log>
    <p class="dk-day"><time datetime="2026-10-14">Today</time></p>
    <div class="dk-msg"><p class="sr-only">Juno Reyes</p><p class="dk-b"><span>Render finished. 4K export is 2.3 GB, uploading now.</span> <time datetime="2026-10-14T23:08">23:08</time></p><p class="dk-b"><span>The colour grade on shot 14 still looks a little green to me.</span> <time datetime="2026-10-14T23:09">23:09</time></p></div>
    <div class="dk-msg out"><p class="sr-only">You</p><p class="dk-b"><span>Agreed. Pull the tint back by 3 and keep the shadows where they are.</span> <time datetime="2026-10-14T23:12">23:12</time></p></div>
    <div class="dk-msg"><p class="sr-only">Juno Reyes</p><p class="dk-b"><span>Done. New link in five minutes.</span> <time datetime="2026-10-14T23:20">23:20</time></p></div>
    <div class="dk-msg out"><p class="sr-only">You</p><p class="dk-b"><span>Perfect, I'll review it before the client call at 10.</span> <time datetime="2026-10-14T23:21">23:21</time></p><p class="dk-rcpt">Read</p></div>
  </div>
  <form class="dk-form" data-form>
    <label class="sr-only" for="dk-input-a">Message to Juno Reyes</label>
    <textarea id="dk-input-a" rows="1" placeholder="Message Juno" data-input></textarea>
    <button class="dk-send" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12 20 4l-6 16-3-7z"/><path d="M11 13 20 4"/></svg><span class="sr-only">Send</span></button>
  </form>
  <template data-tpl><div class="dk-msg out"><p class="sr-only">You</p><p class="dk-b"><span data-text></span> <time data-time></time></p><p class="dk-rcpt">Sent</p></div></template>
</div></div>

<div><span class="lbl">Live-stream chat with a pinned message</span>
<div class="dk stream" data-chat>
  <div class="dk-head">
    <div><h2 id="dk-title-b">Stream chat</h2><p>Night Market Speedrun, 1,284 watching</p></div>
    <span class="dk-live"><i aria-hidden="true"></i>LIVE</span>
  </div>
  <p class="dk-pin"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 17v5M9 3h6l-1 6 4 4H6l4-4z"/></svg><span><b>Pinned by Kai:</b> Giveaway winners are announced at 21:00. Be kind in chat.</span></p>
  <div class="dk-log" role="log" aria-live="polite" aria-labelledby="dk-title-b" tabindex="0" data-log>
    <div class="dk-line"><time datetime="2026-10-14T20:41">20:41</time><p><b class="u1">pixelmoth</b>: that skip on level 3 was clean</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:41">20:41</time><p><span class="dk-tag">MOD</span><b class="u3">Kai</b>: reminder, no spoilers for the final boss</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:42">20:42</time><p><b class="u2">ollie_r</b>: first time catching this live, the route is wild</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:42">20:42</time><p><b class="u4">brightside</b>: 14 seconds ahead of personal best</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:43">20:43</time><p><b class="u5">noor.k</b>: hello from Amman</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:43">20:43</time><p><b class="u1">pixelmoth</b>: here comes the lantern jump</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:44">20:44</time><p><b class="u2">ollie_r</b>: HE MADE IT</p></div>
  </div>
  <form class="dk-form" data-form>
    <label class="sr-only" for="dk-input-b">Send a message to stream chat</label>
    <textarea id="dk-input-b" rows="1" maxlength="200" placeholder="Say something nice" data-input></textarea>
    <span class="dk-count" aria-hidden="true" data-count>0/200</span>
    <button class="dk-chat" type="submit">Chat</button>
  </form>
  <template data-tpl><div class="dk-line"><time data-time></time><p><b class="u5">you</b>: <span data-text></span></p></div></template>
</div></div>
CSS
/* Dark chat: a direct-message thread with gradient sent bubbles, and a compact live-stream chat with coloured names.
   Every colour is checked for contrast on the near-black panel. */
.dk{--dk-bg:#111620;--dk-raise:#1b2230;--dk-line:#262f40;--dk-ink:#e7ebf3;--dk-muted:#9aa5b8;--dk-focus:#c4b5fd;--dk-from:#7c3aed;--dk-to:#2563eb;--dk-online:#4ade80;
  display:flex;flex-direction:column;height:560px;overflow:hidden;background:var(--dk-bg);border:1px solid var(--dk-line);border-radius:20px;color:var(--dk-ink);
  font-size:.93rem;line-height:1.45;box-shadow:0 30px 60px -30px #000}
.dk :where(h2,p){margin:0}
:where(.dk) button{font:inherit;color:inherit;cursor:pointer}
.dk .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}
.dk :focus-visible{outline:2px solid var(--dk-focus);outline-offset:2px}

.dk-head{display:flex;align-items:center;gap:12px;padding:14px 18px;border-bottom:1px solid var(--dk-line)}
.dk-av{position:relative;flex:none;display:grid;place-items:center;width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#f472b6,#a78bfa);color:#1a0b2e;font-size:.78rem;font-weight:800}
.dk-av i{position:absolute;right:-1px;bottom:-1px;width:12px;height:12px;border:2px solid var(--dk-bg);border-radius:50%;background:var(--dk-online)}
.dk-head h2{font-size:1rem;font-weight:700}
.dk-head p{display:flex;align-items:center;gap:6px;font-size:.78rem;color:var(--dk-muted)}

.dk-log{flex:1;display:flex;flex-direction:column;gap:10px;overflow-y:auto;padding:18px 16px}
.dk-log:focus-visible{outline-offset:-3px}
.dk-day{display:flex;align-items:center;gap:10px;font-size:.72rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--dk-muted)}
.dk-day::before,.dk-day::after{content:"";flex:1;height:1px;background:var(--dk-line)}
.dk-msg{display:flex;flex-direction:column;align-items:flex-start;gap:3px;max-width:82%}
.dk-msg.out{align-self:flex-end;align-items:flex-end}
.dk-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(--dk-raise);white-space:pre-wrap;overflow-wrap:anywhere}
.dk-msg.out .dk-b{border-radius:18px 6px 18px 18px;background:linear-gradient(135deg,var(--dk-from),var(--dk-to));color:#fff}
.dk-b time{margin-left:auto;font-size:.7rem;color:var(--dk-muted);white-space:nowrap}
.dk-msg.out .dk-b time{color:#e0e7ff}
.dk-rcpt{font-size:.72rem;color:var(--dk-muted)}

.dk-form{display:flex;align-items:flex-end;gap:8px;padding:12px 14px 14px;border-top:1px solid var(--dk-line)}
.dk-form textarea{box-sizing:border-box;flex:1;min-width:0;max-height:120px;padding:10px 14px;border:1.5px solid #4a556b;border-radius:16px;background:#0b0f17;color:var(--dk-ink);font:inherit;line-height:1.4;resize:none}
.dk-form textarea::placeholder{color:#8f9bb0}
.dk-form textarea:focus-visible{border-color:var(--dk-focus);outline-offset:1px}
.dk-send{flex:none;display:grid;place-items:center;width:44px;height:44px;border:0;border-radius:14px;background:linear-gradient(135deg,var(--dk-from),var(--dk-to));color:#fff;transition:filter .15s}
.dk-send:hover{filter:brightness(1.12)}
.dk-send:disabled{background:#232b3a;color:#8f9bb0;cursor:not-allowed;filter:none}

/* live-stream chat */
.dk.stream{height:560px}
.dk-live{display:inline-flex;align-items:center;gap:6px;margin-left:auto;padding:3px 9px;border-radius:6px;background:#3b0d14;color:#fda4af;font-size:.72rem;font-weight:800;letter-spacing:.08em}
.dk-live i{width:7px;height:7px;border-radius:50%;background:#fb7185}
.dk-pin{display:flex;gap:10px;margin:12px 14px 0;padding:10px 12px;border:1px solid #3b3270;border-radius:12px;background:#17142b;font-size:.84rem}
.dk-pin svg{flex:none;margin-top:2px;color:var(--dk-focus)}
.dk-pin b{color:var(--dk-focus)}
.dk.stream .dk-log{gap:6px;padding:12px 14px}
.dk-line{display:flex;gap:8px;padding:3px 6px;border-radius:8px}
.dk-line:hover{background:#171d29}
.dk-line time{flex:none;width:36px;padding-top:2px;font-size:.72rem;font-variant-numeric:tabular-nums;color:var(--dk-muted)}
.dk-line p{min-width:0;overflow-wrap:anywhere;white-space:pre-wrap}
.dk-line b{font-weight:700}
.dk-tag{display:inline-block;margin-right:4px;padding:0 5px;border-radius:4px;background:#14532d;color:#bbf7d0;font-size:.66rem;font-weight:800;letter-spacing:.06em;vertical-align:1px}
.u1{color:#f9a8d4}.u2{color:#93c5fd}.u3{color:#86efac}.u4{color:#fcd34d}.u5{color:#c4b5fd}
.dk-count{align-self:center;font-size:.74rem;font-variant-numeric:tabular-nums;color:var(--dk-muted)}
.dk-chat{flex:none;height:44px;padding:0 16px;border:0;border-radius:12px;background:var(--dk-focus);color:#1a1033;font-weight:700}
.dk-chat:hover{background:#ddd6fe}
.dk-chat:disabled{background:#232b3a;color:#8f9bb0;cursor:not-allowed}
@media (prefers-reduced-motion:reduce){.dk-send{transition:none}}
JavaScript
/* Dark chats: grow the textarea, Enter sends and Shift+Enter adds a line, Send is enabled only when there is text,
   and an optional character count follows the maxlength. Sending only appends to the page. */
document.querySelectorAll('[data-chat]').forEach(function (chat) {
  var log = chat.querySelector('[data-log]'), form = chat.querySelector('[data-form]'), input = chat.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]'), count = chat.querySelector('[data-count]');
  var pad = function (n) { return String(n).padStart(2, '0'); };
  function update() {
    input.style.height = 'auto'; input.style.height = input.scrollHeight + 'px';
    send.disabled = !input.value.trim();
    if (count) count.textContent = input.value.length + '/' + input.maxLength;
  }
  input.addEventListener('input', update);
  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;
    var d = new Date(), msg = chat.querySelector('template[data-tpl]').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;
    input.value = ''; update(); input.focus();
  });
  log.scrollTop = log.scrollHeight;
  update();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Direct messages with gradient bubbles</span>
<div class="card dk" data-chat>
  <div class="dk-head">
    <span class="dk-av" aria-hidden="true">JR<i></i></span>
    <div><h2 id="dk-title-a">Juno Reyes</h2><p>Online</p></div>
  </div>
  <div class="dk-log" role="log" aria-live="polite" aria-labelledby="dk-title-a" tabindex="0" data-log>
    <p class="dk-day"><time datetime="2026-10-14">Today</time></p>
    <div class="dk-msg"><p class="visually-hidden">Juno Reyes</p><p class="dk-b"><span>Render finished. 4K export is 2.3 GB, uploading now.</span> <time datetime="2026-10-14T23:08">23:08</time></p><p class="dk-b"><span>The colour grade on shot 14 still looks a little green to me.</span> <time datetime="2026-10-14T23:09">23:09</time></p></div>
    <div class="dk-msg out"><p class="visually-hidden">You</p><p class="dk-b"><span>Agreed. Pull the tint back by 3 and keep the shadows where they are.</span> <time datetime="2026-10-14T23:12">23:12</time></p></div>
    <div class="dk-msg"><p class="visually-hidden">Juno Reyes</p><p class="dk-b"><span>Done. New link in five minutes.</span> <time datetime="2026-10-14T23:20">23:20</time></p></div>
    <div class="dk-msg out"><p class="visually-hidden">You</p><p class="dk-b"><span>Perfect, I'll review it before the client call at 10.</span> <time datetime="2026-10-14T23:21">23:21</time></p><p class="dk-rcpt">Read</p></div>
  </div>
  <form class="dk-form" data-form>
    <label class="visually-hidden" for="dk-input-a">Message to Juno Reyes</label>
    <textarea class="form-control" id="dk-input-a" rows="1" placeholder="Message Juno" data-input></textarea>
    <button class="btn dk-send" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12 20 4l-6 16-3-7z"/><path d="M11 13 20 4"/></svg><span class="visually-hidden">Send</span></button>
  </form>
  <template data-tpl><div class="dk-msg out"><p class="visually-hidden">You</p><p class="dk-b"><span data-text></span> <time data-time></time></p><p class="dk-rcpt">Sent</p></div></template>
</div></div>

<div><span class="lbl">Live-stream chat with a pinned message</span>
<div class="card dk stream" data-chat>
  <div class="dk-head">
    <div><h2 id="dk-title-b">Stream chat</h2><p>Night Market Speedrun, 1,284 watching</p></div>
    <span class="badge dk-live"><i aria-hidden="true"></i>LIVE</span>
  </div>
  <p class="dk-pin"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 17v5M9 3h6l-1 6 4 4H6l4-4z"/></svg><span><b>Pinned by Kai:</b> Giveaway winners are announced at 21:00. Be kind in chat.</span></p>
  <div class="dk-log" role="log" aria-live="polite" aria-labelledby="dk-title-b" tabindex="0" data-log>
    <div class="dk-line"><time datetime="2026-10-14T20:41">20:41</time><p><b class="u1">pixelmoth</b>: that skip on level 3 was clean</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:41">20:41</time><p><span class="badge dk-tag">MOD</span><b class="u3">Kai</b>: reminder, no spoilers for the final boss</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:42">20:42</time><p><b class="u2">ollie_r</b>: first time catching this live, the route is wild</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:42">20:42</time><p><b class="u4">brightside</b>: 14 seconds ahead of personal best</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:43">20:43</time><p><b class="u5">noor.k</b>: hello from Amman</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:43">20:43</time><p><b class="u1">pixelmoth</b>: here comes the lantern jump</p></div>
    <div class="dk-line"><time datetime="2026-10-14T20:44">20:44</time><p><b class="u2">ollie_r</b>: HE MADE IT</p></div>
  </div>
  <form class="dk-form" data-form>
    <label class="visually-hidden" for="dk-input-b">Send a message to stream chat</label>
    <textarea class="form-control" id="dk-input-b" rows="1" maxlength="200" placeholder="Say something nice" data-input></textarea>
    <span class="dk-count" aria-hidden="true" data-count>0/200</span>
    <button class="btn dk-chat" type="submit">Chat</button>
  </form>
  <template data-tpl><div class="dk-line"><time data-time></time><p><b class="u5">you</b>: <span data-text></span></p></div></template>
</div></div>
CSS
/* Dark chat in Bootstrap's dark colour mode (data-bs-theme="dark"): .card, .badge, .form-control and .btn, restyled through CSS variables. A direct-message thread with gradient sent bubbles, and a compact live-stream chat with coloured names.
   Every colour is checked for contrast on the near-black panel. */
.dk{--dk-bg:#111620;--dk-raise:#1b2230;--dk-line:#262f40;--dk-ink:#e7ebf3;--dk-muted:#9aa5b8;--dk-focus:#c4b5fd;--dk-from:#7c3aed;--dk-to:#2563eb;--dk-online:#4ade80;
  --bs-card-bg:var(--dk-bg);--bs-card-border-color:var(--dk-line);--bs-card-border-radius:20px;--bs-card-color:var(--dk-ink);height:560px;overflow:hidden;
  font-size:.93rem;line-height:1.45;box-shadow:0 30px 60px -30px #000}
.dk :where(h2,p){margin:0}
.dk :focus-visible{outline:2px solid var(--dk-focus);outline-offset:2px;box-shadow:none}
.dk .btn{--bs-btn-focus-box-shadow:none;--bs-btn-border-width:0;--bs-btn-padding-y:0}

.dk-head{display:flex;align-items:center;gap:12px;padding:14px 18px;border-bottom:1px solid var(--dk-line)}
.dk-av{position:relative;flex:none;display:grid;place-items:center;width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#f472b6,#a78bfa);color:#1a0b2e;font-size:.78rem;font-weight:800}
.dk-av i{position:absolute;right:-1px;bottom:-1px;width:12px;height:12px;border:2px solid var(--dk-bg);border-radius:50%;background:var(--dk-online)}
.dk-head h2{font-size:1rem;font-weight:700}
.dk-head p{display:flex;align-items:center;gap:6px;font-size:.78rem;color:var(--dk-muted)}

.dk-log{flex:1;display:flex;flex-direction:column;gap:10px;overflow-y:auto;padding:18px 16px}
.dk-log:focus-visible{outline-offset:-3px}
.dk-day{display:flex;align-items:center;gap:10px;font-size:.72rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--dk-muted)}
.dk-day::before,.dk-day::after{content:"";flex:1;height:1px;background:var(--dk-line)}
.dk-msg{display:flex;flex-direction:column;align-items:flex-start;gap:3px;max-width:82%}
.dk-msg.out{align-self:flex-end;align-items:flex-end}
.dk-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(--dk-raise);white-space:pre-wrap;overflow-wrap:anywhere}
.dk-msg.out .dk-b{border-radius:18px 6px 18px 18px;background:linear-gradient(135deg,var(--dk-from),var(--dk-to));color:#fff}
.dk-b time{margin-left:auto;font-size:.7rem;color:var(--dk-muted);white-space:nowrap}
.dk-msg.out .dk-b time{color:#e0e7ff}
.dk-rcpt{font-size:.72rem;color:var(--dk-muted)}

.dk-form{display:flex;align-items:flex-end;gap:8px;padding:12px 14px 14px;border-top:1px solid var(--dk-line)}
.dk-form .form-control{--bs-border-color:#4a556b;--bs-body-bg:#0b0f17;max-height:120px;padding:10px 14px;border-width:1.5px;border-radius:16px;background:#0b0f17;color:var(--dk-ink);font-size:inherit;line-height:1.4;resize:none}
.dk-form .form-control::placeholder{color:#8f9bb0}
.dk-form .form-control:focus{border-color:var(--dk-focus);box-shadow:none}
.dk-form .form-control:focus-visible{outline-offset:1px}
.dk-send{--bs-btn-padding-x:0;--bs-btn-border-radius:14px;--bs-btn-color:#fff;--bs-btn-hover-color:#fff;--bs-btn-active-color:#fff;--bs-btn-disabled-color:#8f9bb0;--bs-btn-disabled-bg:#232b3a;--bs-btn-disabled-opacity:1;display:grid;place-items:center;flex:none;width:44px;height:44px;background-image:linear-gradient(135deg,var(--dk-from),var(--dk-to));transition:filter .15s}
.dk-send:hover{filter:brightness(1.12)}
.dk-send:disabled{background-image:none;filter:none}

/* live-stream chat */
.dk.stream{height:560px}
.dk-live{--bs-badge-padding-x:9px;--bs-badge-padding-y:4px;--bs-badge-font-size:.72rem;--bs-badge-font-weight:800;--bs-badge-color:#fda4af;--bs-badge-border-radius:6px;display:inline-flex;align-items:center;gap:6px;margin-left:auto;background:#3b0d14;letter-spacing:.08em}
.dk-live i{width:7px;height:7px;border-radius:50%;background:#fb7185}
.dk-pin{display:flex;gap:10px;margin:12px 14px 0;padding:10px 12px;border:1px solid #3b3270;border-radius:12px;background:#17142b;font-size:.84rem}
.dk-pin svg{flex:none;margin-top:2px;color:var(--dk-focus)}
.dk-pin b{color:var(--dk-focus)}
.dk.stream .dk-log{gap:6px;padding:12px 14px}
.dk-line{display:flex;gap:8px;padding:3px 6px;border-radius:8px}
.dk-line:hover{background:#171d29}
.dk-line time{flex:none;width:36px;padding-top:2px;font-size:.72rem;font-variant-numeric:tabular-nums;color:var(--dk-muted)}
.dk-line p{min-width:0;overflow-wrap:anywhere;white-space:pre-wrap}
.dk-line b{font-weight:700}
.dk-tag{--bs-badge-padding-x:5px;--bs-badge-padding-y:2px;--bs-badge-font-size:.66rem;--bs-badge-font-weight:800;--bs-badge-color:#bbf7d0;--bs-badge-border-radius:4px;margin-right:4px;background:#14532d;letter-spacing:.06em;vertical-align:1px}
.u1{color:#f9a8d4}.u2{color:#93c5fd}.u3{color:#86efac}.u4{color:#fcd34d}.u5{color:#c4b5fd}
.dk-count{align-self:center;font-size:.74rem;font-variant-numeric:tabular-nums;color:var(--dk-muted)}
.dk-chat{--bs-btn-padding-x:16px;--bs-btn-font-weight:700;--bs-btn-font-size:.93rem;--bs-btn-border-radius:12px;--bs-btn-color:#1a1033;--bs-btn-bg:var(--dk-focus);--bs-btn-hover-color:#1a1033;--bs-btn-hover-bg:#ddd6fe;--bs-btn-active-color:#1a1033;--bs-btn-active-bg:#ddd6fe;--bs-btn-disabled-color:#8f9bb0;--bs-btn-disabled-bg:#232b3a;--bs-btn-disabled-opacity:1;flex:none;height:44px}
@media (prefers-reduced-motion:reduce){.dk-send{transition:none}}
JavaScript
/* Dark chats: grow the textarea, Enter sends and Shift+Enter adds a line, Send is enabled only when there is text,
   and an optional character count follows the maxlength. Sending only appends to the page. */
document.querySelectorAll('[data-chat]').forEach(function (chat) {
  var log = chat.querySelector('[data-log]'), form = chat.querySelector('[data-form]'), input = chat.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]'), count = chat.querySelector('[data-count]');
  var pad = function (n) { return String(n).padStart(2, '0'); };
  function update() {
    input.style.height = 'auto'; input.style.height = input.scrollHeight + 'px';
    send.disabled = !input.value.trim();
    if (count) count.textContent = input.value.length + '/' + input.maxLength;
  }
  input.addEventListener('input', update);
  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;
    var d = new Date(), msg = chat.querySelector('template[data-tpl]').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;
    input.value = ''; update(); input.focus();
  });
  log.scrollTop = log.scrollHeight;
  update();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-dk-muted uppercase">Direct messages with gradient bubbles</span>
<div class="flex h-[560px] flex-col overflow-hidden rounded-[20px] border border-dk-line bg-dk-bg text-[.93rem] leading-[1.45] text-dk-ink shadow-[0_30px_60px_-30px_#000]" data-chat>
  <div class="flex items-center gap-3 border-b border-dk-line px-[18px] py-3.5">
    <span class="relative grid size-10 flex-none place-items-center rounded-full bg-linear-135 from-[#f472b6] to-[#a78bfa] text-[.78rem] font-extrabold text-[#1a0b2e]" aria-hidden="true">JR<i class="absolute -right-px -bottom-px size-3 rounded-full border-2 border-dk-bg bg-[#4ade80]"></i></span>
    <div><h2 class="text-base font-bold" id="dk-title-a">Juno Reyes</h2><p class="text-[.78rem] text-dk-muted">Online</p></div>
  </div>
  <div class="flex flex-1 flex-col gap-2.5 overflow-y-auto px-4 py-[18px] focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-dk-focus" role="log" aria-live="polite" aria-labelledby="dk-title-a" tabindex="0" data-log>
    <p class="flex items-center gap-2.5 text-[.72rem] font-semibold tracking-[.06em] text-dk-muted uppercase before:h-px before:flex-1 before:bg-dk-line after:h-px after:flex-1 after:bg-dk-line"><time class="ml-auto text-[.7rem] whitespace-nowrap text-dk-muted" datetime="2026-10-14">Today</time></p>
    <div class="flex max-w-[82%] flex-col items-start gap-[3px]"><p class="sr-only">Juno Reyes</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_18px] bg-dk-raise px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>Render finished. 4K export is 2.3 GB, uploading now.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-dk-muted" datetime="2026-10-14T23:08">23:08</time></p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_18px] bg-dk-raise px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>The colour grade on shot 14 still looks a little green to me.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-dk-muted" datetime="2026-10-14T23:09">23:09</time></p></div>
    <div class="flex max-w-[82%] 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-linear-135 from-dk-from to-dk-to px-[13px] pt-2 pb-[7px] whitespace-pre-wrap text-white [overflow-wrap:anywhere]"><span>Agreed. Pull the tint back by 3 and keep the shadows where they are.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-[#e0e7ff]" datetime="2026-10-14T23:12">23:12</time></p></div>
    <div class="flex max-w-[82%] flex-col items-start gap-[3px]"><p class="sr-only">Juno Reyes</p><p class="flex flex-wrap items-end gap-x-2.5 rounded-[6px_18px_18px_18px] bg-dk-raise px-[13px] pt-2 pb-[7px] whitespace-pre-wrap [overflow-wrap:anywhere]"><span>Done. New link in five minutes.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-dk-muted" datetime="2026-10-14T23:20">23:20</time></p></div>
    <div class="flex max-w-[82%] 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-linear-135 from-dk-from to-dk-to px-[13px] pt-2 pb-[7px] whitespace-pre-wrap text-white [overflow-wrap:anywhere]"><span>Perfect, I'll review it before the client call at 10.</span> <time class="ml-auto text-[.7rem] whitespace-nowrap text-[#e0e7ff]" datetime="2026-10-14T23:21">23:21</time></p><p class="text-[.72rem] text-dk-muted">Read</p></div>
  </div>
  <form class="flex items-end gap-2 border-t border-dk-line px-3.5 pt-3 pb-3.5" data-form>
    <label class="sr-only" for="dk-input-a">Message to Juno Reyes</label>
    <textarea class="box-border max-h-[120px] min-w-0 flex-1 resize-none rounded-2xl border-[1.5px] border-[#4a556b] bg-[#0b0f17] px-3.5 py-2.5 leading-[1.4] text-dk-ink placeholder:text-[#8f9bb0] focus-visible:border-dk-focus focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dk-focus" id="dk-input-a" rows="1" placeholder="Message Juno" data-input></textarea>
    <button class="grid size-11 flex-none cursor-pointer place-items-center rounded-[14px] bg-linear-135 from-dk-from to-dk-to text-white transition-[filter] duration-150 hover:brightness-112 disabled:cursor-not-allowed disabled:bg-none disabled:bg-[#232b3a] disabled:text-[#8f9bb0] disabled:brightness-100 motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dk-focus" type="submit"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12 20 4l-6 16-3-7z"/><path d="M11 13 20 4"/></svg><span class="sr-only">Send</span></button>
  </form>
  <template data-tpl><div class="flex max-w-[82%] 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-linear-135 from-dk-from to-dk-to 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-[#e0e7ff]" data-time></time></p><p class="text-[.72rem] text-dk-muted">Sent</p></div></template>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-dk-muted uppercase">Live-stream chat with a pinned message</span>
<div class="flex h-[560px] flex-col overflow-hidden rounded-[20px] border border-dk-line bg-dk-bg text-[.93rem] leading-[1.45] text-dk-ink shadow-[0_30px_60px_-30px_#000]" data-chat>
  <div class="flex items-center gap-3 border-b border-dk-line px-[18px] py-3.5">
    <div><h2 class="text-base font-bold" id="dk-title-b">Stream chat</h2><p class="text-[.78rem] text-dk-muted">Night Market Speedrun, 1,284 watching</p></div>
    <span class="ml-auto inline-flex items-center gap-1.5 rounded-md bg-[#3b0d14] px-[9px] py-[3px] text-[.72rem] font-extrabold tracking-[.08em] text-[#fda4af]"><i class="size-[7px] rounded-full bg-[#fb7185]" aria-hidden="true"></i>LIVE</span>
  </div>
  <p class="mx-3.5 mt-3 flex gap-2.5 rounded-xl border border-[#3b3270] bg-[#17142b] px-3 py-2.5 text-[.84rem] [&>svg]:mt-0.5 [&>svg]:flex-none [&>svg]:text-dk-focus"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 17v5M9 3h6l-1 6 4 4H6l4-4z"/></svg><span><b class="text-dk-focus">Pinned by Kai:</b> Giveaway winners are announced at 21:00. Be kind in chat.</span></p>
  <div class="flex flex-1 flex-col gap-1.5 overflow-y-auto px-3.5 py-3 focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-dk-focus" role="log" aria-live="polite" aria-labelledby="dk-title-b" tabindex="0" data-log>
    <div class="flex gap-2 rounded-lg px-1.5 py-[3px] hover:bg-[#171d29]"><time class="w-9 flex-none pt-0.5 text-[.72rem] text-dk-muted tabular-nums" datetime="2026-10-14T20:41">20:41</time><p class="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]"><b class="font-bold text-[#f9a8d4]">pixelmoth</b>: that skip on level 3 was clean</p></div>
    <div class="flex gap-2 rounded-lg px-1.5 py-[3px] hover:bg-[#171d29]"><time class="w-9 flex-none pt-0.5 text-[.72rem] text-dk-muted tabular-nums" datetime="2026-10-14T20:41">20:41</time><p class="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]"><span class="mr-1 inline-block rounded bg-[#14532d] px-[5px] align-[1px] text-[.66rem] font-extrabold tracking-[.06em] text-[#bbf7d0]">MOD</span><b class="font-bold text-[#86efac]">Kai</b>: reminder, no spoilers for the final boss</p></div>
    <div class="flex gap-2 rounded-lg px-1.5 py-[3px] hover:bg-[#171d29]"><time class="w-9 flex-none pt-0.5 text-[.72rem] text-dk-muted tabular-nums" datetime="2026-10-14T20:42">20:42</time><p class="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]"><b class="font-bold text-[#93c5fd]">ollie_r</b>: first time catching this live, the route is wild</p></div>
    <div class="flex gap-2 rounded-lg px-1.5 py-[3px] hover:bg-[#171d29]"><time class="w-9 flex-none pt-0.5 text-[.72rem] text-dk-muted tabular-nums" datetime="2026-10-14T20:42">20:42</time><p class="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]"><b class="font-bold text-[#fcd34d]">brightside</b>: 14 seconds ahead of personal best</p></div>
    <div class="flex gap-2 rounded-lg px-1.5 py-[3px] hover:bg-[#171d29]"><time class="w-9 flex-none pt-0.5 text-[.72rem] text-dk-muted tabular-nums" datetime="2026-10-14T20:43">20:43</time><p class="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]"><b class="font-bold text-[#c4b5fd]">noor.k</b>: hello from Amman</p></div>
    <div class="flex gap-2 rounded-lg px-1.5 py-[3px] hover:bg-[#171d29]"><time class="w-9 flex-none pt-0.5 text-[.72rem] text-dk-muted tabular-nums" datetime="2026-10-14T20:43">20:43</time><p class="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]"><b class="font-bold text-[#f9a8d4]">pixelmoth</b>: here comes the lantern jump</p></div>
    <div class="flex gap-2 rounded-lg px-1.5 py-[3px] hover:bg-[#171d29]"><time class="w-9 flex-none pt-0.5 text-[.72rem] text-dk-muted tabular-nums" datetime="2026-10-14T20:44">20:44</time><p class="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]"><b class="font-bold text-[#93c5fd]">ollie_r</b>: HE MADE IT</p></div>
  </div>
  <form class="flex items-end gap-2 border-t border-dk-line px-3.5 pt-3 pb-3.5" data-form>
    <label class="sr-only" for="dk-input-b">Send a message to stream chat</label>
    <textarea class="box-border max-h-[120px] min-w-0 flex-1 resize-none rounded-2xl border-[1.5px] border-[#4a556b] bg-[#0b0f17] px-3.5 py-2.5 leading-[1.4] text-dk-ink placeholder:text-[#8f9bb0] focus-visible:border-dk-focus focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-dk-focus" id="dk-input-b" rows="1" maxlength="200" placeholder="Say something nice" data-input></textarea>
    <span class="self-center text-[.74rem] text-dk-muted tabular-nums" aria-hidden="true" data-count>0/200</span>
    <button class="h-11 flex-none cursor-pointer rounded-xl bg-dk-focus px-4 font-bold text-[#1a1033] hover:bg-[#ddd6fe] disabled:cursor-not-allowed disabled:bg-[#232b3a] disabled:text-[#8f9bb0] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dk-focus" type="submit">Chat</button>
  </form>
  <template data-tpl><div class="flex gap-2 rounded-lg px-1.5 py-[3px] hover:bg-[#171d29]"><time class="w-9 flex-none pt-0.5 text-[.72rem] text-dk-muted tabular-nums" data-time></time><p class="min-w-0 whitespace-pre-wrap [overflow-wrap:anywhere]"><b class="font-bold text-[#c4b5fd]">you</b>: <span data-text></span></p></div></template>
</div></div>
CSS
/* Dark chat palette as theme colours, checked for contrast on the near-black panel. */
@theme {
  --color-dk-bg:#111620; --color-dk-raise:#1b2230; --color-dk-line:#262f40; --color-dk-ink:#e7ebf3; --color-dk-muted:#9aa5b8;
  --color-dk-focus:#c4b5fd; --color-dk-from:#7c3aed; --color-dk-to:#2563eb;
}
JavaScript
/* Dark chats: grow the textarea, Enter sends and Shift+Enter adds a line, Send is enabled only when there is text,
   and an optional character count follows the maxlength. Sending only appends to the page. */
document.querySelectorAll('[data-chat]').forEach(function (chat) {
  var log = chat.querySelector('[data-log]'), form = chat.querySelector('[data-form]'), input = chat.querySelector('[data-input]');
  var send = form.querySelector('[type="submit"]'), count = chat.querySelector('[data-count]');
  var pad = function (n) { return String(n).padStart(2, '0'); };
  function update() {
    input.style.height = 'auto'; input.style.height = input.scrollHeight + 'px';
    send.disabled = !input.value.trim();
    if (count) count.textContent = input.value.length + '/' + input.maxLength;
  }
  input.addEventListener('input', update);
  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;
    var d = new Date(), msg = chat.querySelector('template[data-tpl]').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;
    input.value = ''; update(); input.focus();
  });
  log.scrollTop = log.scrollHeight;
  update();
});

About this set

Two chat interfaces for dark products. Direct messages pairs a dark grey received bubble with a violet-to-blue gradient for your messages, an online dot on the contact’s avatar, a date separator and a Read receipt written as text. Live-stream chat is the compact layout of streaming and event sites: a LIVE badge, a pinned moderator message, one line per message with a time, a coloured name and a MOD tag, and a composer with a character count and a Chat button. Every text colour, including the five name colours, the gradient endpoints and the muted timestamps, reaches at least 4.5:1 contrast against its background, and focus rings use a light violet that stands out on the near-black panel. Both share one short script: the textarea grows, Enter sends, Shift+Enter adds a line, the button stays disabled while the field is empty and the count follows maxlength. The Bootstrap file runs in data-bs-theme=”dark” with .card, .badge, .form-control and .btn. For a very busy stream, consider turning the live region off and offering a pause button instead.

What’s included

  • Gradient sent bubbles and dark received bubbles
  • Live-stream chat with a pinned message and a MOD tag
  • Name colours and timestamps at 4.5:1 contrast or better
  • Character count, Enter to send and a disabled empty state
  • Bootstrap version in data-bs-theme="dark"

Accessibility

Both chats are focusable role="log" regions with aria-live="polite". Every message has a time element and a sender, written as a hidden name in the direct-message thread, and the LIVE and MOD labels are text. Textareas have hidden labels, focus outlines are light violet for contrast on the dark panel, and the only transition, on the Send button, is removed under prefers-reduced-motion.

Customise it

Change --dk-from and --dk-to for the bubble gradient, --dk-raise and --dk-bg for the surfaces, and the .u1 to .u5 name colours. In Tailwind edit the --color-dk-* theme colours; Bootstrap reads the same variables on top of its dark colour mode.