Open demo

CSS code

HTML
<div><span class="lbl">In a slot 420px or wider: avatars and a labelled Send button</span>
<div class="mc" data-chat>
  <div class="mc-head"><div><h2 id="mc-title-a">Ridge Loop hike</h2><p>Ana, Ben and you</p></div></div>
  <div class="mc-log" role="log" aria-live="polite" aria-labelledby="mc-title-a" tabindex="0">
    <div class="mc-stack" data-log>
      <p class="mc-day"><time datetime="2026-10-14">Today</time></p>
      <div class="mc-msg"><span class="mc-av" aria-hidden="true">AL</span><div class="mc-col"><p class="mc-who">Ana Lima</p><p class="mc-b">Forecast says dry until 15:00. Start at 07:30 from the north car park?</p><p class="mc-meta"><time datetime="2026-10-14T18:02">18:02</time></p></div></div>
      <div class="mc-msg"><span class="mc-av c2" aria-hidden="true">BC</span><div class="mc-col"><p class="mc-who">Ben Cho</p><p class="mc-b">Works for me. I can bring the stove and coffee for the summit.</p><p class="mc-meta"><time datetime="2026-10-14T18:05">18:05</time></p></div></div>
      <div class="mc-msg out"><div class="mc-col"><p class="sr-only">You</p><p class="mc-b">I'll print the map in case the signal drops on the east ridge.</p><p class="mc-meta"><time datetime="2026-10-14T18:07">18:07</time> · Read</p></div></div>
      <div class="mc-msg"><span class="mc-av" aria-hidden="true">AL</span><div class="mc-col"><p class="mc-who">Ana Lima</p><p class="mc-b">Perfect. 14 km and about 650 m of climbing in total.</p><p class="mc-meta"><time datetime="2026-10-14T18:09">18:09</time></p></div></div>
    </div>
  </div>
  <form class="mc-form" data-form>
    <label class="sr-only" for="mc-input-a">Message to Ridge Loop hike</label>
    <textarea id="mc-input-a" rows="1" required placeholder="Write a message"></textarea>
    <button class="mc-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="mc-label">Send</span></button>
  </form>
  <template><div class="mc-msg out"><div class="mc-col"><p class="sr-only">You</p><p class="mc-b" data-text></p><p class="mc-meta"><time data-time></time> · Sent</p></div></div></template>
</div></div>

<div><span class="lbl">In a 300px slot: the same markup, compact</span>
<div class="mc" data-chat>
  <div class="mc-head"><div><h2 id="mc-title-b">Ridge Loop hike</h2><p>Ana, Ben and you</p></div></div>
  <div class="mc-log" role="log" aria-live="polite" aria-labelledby="mc-title-b" tabindex="0">
    <div class="mc-stack" data-log>
      <p class="mc-day"><time datetime="2026-10-14">Today</time></p>
      <div class="mc-msg"><span class="mc-av" aria-hidden="true">AL</span><div class="mc-col"><p class="mc-who">Ana Lima</p><p class="mc-b">Forecast says dry until 15:00. Start at 07:30 from the north car park?</p><p class="mc-meta"><time datetime="2026-10-14T18:02">18:02</time></p></div></div>
      <div class="mc-msg"><span class="mc-av c2" aria-hidden="true">BC</span><div class="mc-col"><p class="mc-who">Ben Cho</p><p class="mc-b">Works for me. I can bring the stove and coffee for the summit.</p><p class="mc-meta"><time datetime="2026-10-14T18:05">18:05</time></p></div></div>
      <div class="mc-msg out"><div class="mc-col"><p class="sr-only">You</p><p class="mc-b">I'll print the map in case the signal drops on the east ridge.</p><p class="mc-meta"><time datetime="2026-10-14T18:07">18:07</time> · Read</p></div></div>
      <div class="mc-msg"><span class="mc-av" aria-hidden="true">AL</span><div class="mc-col"><p class="mc-who">Ana Lima</p><p class="mc-b">Perfect. 14 km and about 650 m of climbing in total.</p><p class="mc-meta"><time datetime="2026-10-14T18:09">18:09</time></p></div></div>
    </div>
  </div>
  <form class="mc-form" data-form>
    <label class="sr-only" for="mc-input-b">Message to Ridge Loop hike</label>
    <textarea id="mc-input-b" rows="1" required placeholder="Write a message"></textarea>
    <button class="mc-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="mc-label">Send</span></button>
  </form>
  <template><div class="mc-msg out"><div class="mc-col"><p class="sr-only">You</p><p class="mc-b" data-text></p><p class="mc-meta"><time data-time></time> · Sent</p></div></div></template>
</div></div>
CSS
/* Modern CSS chat. The log is a column-reverse scroller, so it opens at the newest message and stays there as messages arrive.
   New messages fade in with @starting-style, the textarea sizes itself with field-sizing: content, :has() dims Send while the
   field is empty, and a container query switches between a compact and a roomy layout by the width of the slot. */
.mc{--mc-accent:#047857;--mc-accent-dark:#065f46;--mc-soft:#d1fae5;--mc-in:#eef3f1;--mc-ink:#132a22;--mc-muted:#4d6158;--mc-line:#d9e5df;--mc-bg:#fff;
  container:mc/inline-size;display:flex;flex-direction:column;height:500px;overflow:hidden;background:var(--mc-bg);border:1px solid var(--mc-line);
  border-radius:20px;color:var(--mc-ink);font-size:.93rem;line-height:1.45;box-shadow:0 20px 44px -28px rgba(19,42,34,.4)}
.mc :where(h2,p){margin:0}
:where(.mc) button{font:inherit;color:inherit;cursor:pointer}
.mc .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}
.mc :focus-visible{outline:2px solid var(--mc-accent);outline-offset:2px}

.mc-head{display:flex;align-items:center;gap:10px;padding:12px 14px;border-bottom:1px solid var(--mc-line)}
.mc-head h2{font-size:.98rem;font-weight:700}
.mc-head p{font-size:.76rem;color:var(--mc-muted)}

/* column-reverse: the scroll origin is the bottom edge, so the newest message stays in view without script */
.mc-log{flex:1;display:flex;flex-direction:column-reverse;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable;padding:14px 10px}
.mc-log:focus-visible{outline-offset:-3px}
.mc-stack{display:flex;flex-direction:column;gap:10px}
.mc-day{align-self:center;padding:3px 10px;border-radius:999px;background:var(--mc-in);font-size:.72rem;font-weight:600;color:var(--mc-muted)}
.mc-msg{display:flex;gap:8px;align-items:flex-end;max-width:92%;transition:opacity .3s ease-out,translate .3s ease-out}
@starting-style{.mc-msg{opacity:0;translate:0 10px}}
.mc-msg.out{align-self:flex-end;flex-direction:row-reverse}
.mc-av{display:none;flex:none;place-items:center;width:30px;height:30px;border-radius:50%;background:#fde68a;color:#713f12;font-size:.68rem;font-weight:700}
.mc-av.c2{background:#bfdbfe;color:#1e3a8a}
.mc-col{display:flex;flex-direction:column;align-items:flex-start;gap:2px;min-width:0}
.mc-msg.out .mc-col{align-items:flex-end}
.mc-who{margin-left:10px;font-size:.72rem;font-weight:600;color:var(--mc-muted)}
.mc-b{padding:7px 12px;border-radius:6px 16px 16px 16px;background:var(--mc-in);text-wrap:pretty;overflow-wrap:anywhere;white-space:pre-wrap}
.mc-msg.out .mc-b{border-radius:16px 6px 16px 16px;background:var(--mc-accent);color:#fff}
.mc-meta{padding:0 6px;font-size:.68rem;color:var(--mc-muted)}

/* composer: field-sizing grows the textarea, :has() dims Send while it is empty (required makes an empty field :invalid) */
.mc-form{display:flex;align-items:flex-end;gap:8px;padding:10px 12px 12px;border-top:1px solid var(--mc-line)}
.mc-form textarea{box-sizing:border-box;flex:1;min-width:0;field-sizing:content;min-height:42px;max-height:132px;padding:10px 14px;border:1.5px solid #82958c;border-radius:14px;
  background:var(--mc-bg);color:var(--mc-ink);font:inherit;line-height:1.4;resize:none}
.mc-form textarea::placeholder{color:#5c6f66}
.mc-form textarea:focus-visible{border-color:var(--mc-accent);outline-offset:1px}
.mc-send{flex:none;display:flex;align-items:center;justify-content:center;gap:6px;min-width:42px;height:42px;padding:0 11px;border:0;border-radius:14px;background:var(--mc-accent);color:#fff;font-weight:600;transition:background-color .2s,color .2s}
.mc-send:hover{background:var(--mc-accent-dark)}
.mc-form:has(textarea:invalid) .mc-send{background:#dce5e1;color:#4d6158}
.mc-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}

/* roomy layout when the slot is at least 420px wide */
@container mc (min-width:420px){
  .mc-head{padding:14px 20px}
  .mc-log{padding:18px 16px}
  .mc-msg{max-width:78%}
  .mc-av{display:grid}
  .mc-label{position:static;width:auto;height:auto;overflow:visible;clip:auto}
  .mc-send{padding:0 16px 0 18px}
  .mc-form{padding:12px 16px 16px}
}
@media (prefers-reduced-motion:reduce){.mc-msg,.mc-send{transition:none}}
JavaScript
/* The only script: append the message and handle Enter (Shift+Enter = new line). Growing, the Send state,
   staying scrolled to the newest message and the entrance animation are all CSS. Nothing leaves the page. */
document.querySelectorAll('[data-chat]').forEach(function (chat) {
  var form = chat.querySelector('[data-form]'), input = form.querySelector('textarea'), log = chat.querySelector('[data-log]');
  var pad = function (n) { return String(n).padStart(2, '0'); };
  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').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);
    input.value = ''; input.focus();
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">In a slot 420px or wider: avatars and a labelled Send button</span>
<div class="card mc" data-chat>
  <div class="mc-head"><div><h2 id="mc-title-a">Ridge Loop hike</h2><p>Ana, Ben and you</p></div></div>
  <div class="mc-log" role="log" aria-live="polite" aria-labelledby="mc-title-a" tabindex="0">
    <div class="mc-stack" data-log>
      <p class="badge rounded-pill mc-day"><time datetime="2026-10-14">Today</time></p>
      <div class="mc-msg"><span class="mc-av" aria-hidden="true">AL</span><div class="mc-col"><p class="mc-who">Ana Lima</p><p class="mc-b">Forecast says dry until 15:00. Start at 07:30 from the north car park?</p><p class="mc-meta"><time datetime="2026-10-14T18:02">18:02</time></p></div></div>
      <div class="mc-msg"><span class="mc-av c2" aria-hidden="true">BC</span><div class="mc-col"><p class="mc-who">Ben Cho</p><p class="mc-b">Works for me. I can bring the stove and coffee for the summit.</p><p class="mc-meta"><time datetime="2026-10-14T18:05">18:05</time></p></div></div>
      <div class="mc-msg out"><div class="mc-col"><p class="visually-hidden">You</p><p class="mc-b">I'll print the map in case the signal drops on the east ridge.</p><p class="mc-meta"><time datetime="2026-10-14T18:07">18:07</time> · Read</p></div></div>
      <div class="mc-msg"><span class="mc-av" aria-hidden="true">AL</span><div class="mc-col"><p class="mc-who">Ana Lima</p><p class="mc-b">Perfect. 14 km and about 650 m of climbing in total.</p><p class="mc-meta"><time datetime="2026-10-14T18:09">18:09</time></p></div></div>
    </div>
  </div>
  <form class="mc-form" data-form>
    <label class="visually-hidden" for="mc-input-a">Message to Ridge Loop hike</label>
    <textarea class="form-control" id="mc-input-a" rows="1" required placeholder="Write a message"></textarea>
    <button class="btn mc-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="mc-label">Send</span></button>
  </form>
  <template><div class="mc-msg out"><div class="mc-col"><p class="visually-hidden">You</p><p class="mc-b" data-text></p><p class="mc-meta"><time data-time></time> · Sent</p></div></div></template>
</div></div>

<div><span class="lbl">In a 300px slot: the same markup, compact</span>
<div class="card mc" data-chat>
  <div class="mc-head"><div><h2 id="mc-title-b">Ridge Loop hike</h2><p>Ana, Ben and you</p></div></div>
  <div class="mc-log" role="log" aria-live="polite" aria-labelledby="mc-title-b" tabindex="0">
    <div class="mc-stack" data-log>
      <p class="badge rounded-pill mc-day"><time datetime="2026-10-14">Today</time></p>
      <div class="mc-msg"><span class="mc-av" aria-hidden="true">AL</span><div class="mc-col"><p class="mc-who">Ana Lima</p><p class="mc-b">Forecast says dry until 15:00. Start at 07:30 from the north car park?</p><p class="mc-meta"><time datetime="2026-10-14T18:02">18:02</time></p></div></div>
      <div class="mc-msg"><span class="mc-av c2" aria-hidden="true">BC</span><div class="mc-col"><p class="mc-who">Ben Cho</p><p class="mc-b">Works for me. I can bring the stove and coffee for the summit.</p><p class="mc-meta"><time datetime="2026-10-14T18:05">18:05</time></p></div></div>
      <div class="mc-msg out"><div class="mc-col"><p class="visually-hidden">You</p><p class="mc-b">I'll print the map in case the signal drops on the east ridge.</p><p class="mc-meta"><time datetime="2026-10-14T18:07">18:07</time> · Read</p></div></div>
      <div class="mc-msg"><span class="mc-av" aria-hidden="true">AL</span><div class="mc-col"><p class="mc-who">Ana Lima</p><p class="mc-b">Perfect. 14 km and about 650 m of climbing in total.</p><p class="mc-meta"><time datetime="2026-10-14T18:09">18:09</time></p></div></div>
    </div>
  </div>
  <form class="mc-form" data-form>
    <label class="visually-hidden" for="mc-input-b">Message to Ridge Loop hike</label>
    <textarea class="form-control" id="mc-input-b" rows="1" required placeholder="Write a message"></textarea>
    <button class="btn mc-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="mc-label">Send</span></button>
  </form>
  <template><div class="mc-msg out"><div class="mc-col"><p class="visually-hidden">You</p><p class="mc-b" data-text></p><p class="mc-meta"><time data-time></time> · Sent</p></div></div></template>
</div></div>
CSS
/* Modern CSS chat on Bootstrap: a .card with a .form-control textarea and a .btn. The log is a column-reverse scroller, so it opens at the newest message and stays there as messages arrive.
   New messages fade in with @starting-style, the textarea sizes itself with field-sizing: content, :has() dims Send while the
   field is empty, and a container query switches between a compact and a roomy layout by the width of the slot. */
.mc{--mc-accent:#047857;--mc-accent-dark:#065f46;--mc-soft:#d1fae5;--mc-in:#eef3f1;--mc-ink:#132a22;--mc-muted:#4d6158;--mc-line:#d9e5df;--mc-bg:#fff;
  --bs-card-bg:var(--mc-bg);--bs-card-border-color:var(--mc-line);--bs-card-border-radius:20px;--bs-card-color:var(--mc-ink);
  container:mc/inline-size;height:500px;overflow:hidden;font-size:.93rem;line-height:1.45;box-shadow:0 20px 44px -28px rgba(19,42,34,.4)}
.mc :where(h2,p){margin:0}
.mc :focus-visible{outline:2px solid var(--mc-accent);outline-offset:2px;box-shadow:none}

.mc-head{display:flex;align-items:center;gap:10px;padding:12px 14px;border-bottom:1px solid var(--mc-line)}
.mc-head h2{font-size:.98rem;font-weight:700}
.mc-head p{font-size:.76rem;color:var(--mc-muted)}

/* column-reverse: the scroll origin is the bottom edge, so the newest message stays in view without script */
.mc-log{flex:1;display:flex;flex-direction:column-reverse;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable;padding:14px 10px}
.mc-log:focus-visible{outline-offset:-3px}
.mc-stack{display:flex;flex-direction:column;gap:10px}
.mc-day{--bs-badge-padding-x:10px;--bs-badge-padding-y:4px;--bs-badge-font-size:.72rem;--bs-badge-font-weight:600;--bs-badge-color:var(--mc-muted);align-self:center;background:var(--mc-in)}
.mc-msg{display:flex;gap:8px;align-items:flex-end;max-width:92%;transition:opacity .3s ease-out,translate .3s ease-out}
@starting-style{.mc-msg{opacity:0;translate:0 10px}}
.mc-msg.out{align-self:flex-end;flex-direction:row-reverse}
.mc-av{display:none;flex:none;place-items:center;width:30px;height:30px;border-radius:50%;background:#fde68a;color:#713f12;font-size:.68rem;font-weight:700}
.mc-av.c2{background:#bfdbfe;color:#1e3a8a}
.mc-col{display:flex;flex-direction:column;align-items:flex-start;gap:2px;min-width:0}
.mc-msg.out .mc-col{align-items:flex-end}
.mc-who{margin-left:10px;font-size:.72rem;font-weight:600;color:var(--mc-muted)}
.mc-b{padding:7px 12px;border-radius:6px 16px 16px 16px;background:var(--mc-in);text-wrap:pretty;overflow-wrap:anywhere;white-space:pre-wrap}
.mc-msg.out .mc-b{border-radius:16px 6px 16px 16px;background:var(--mc-accent);color:#fff}
.mc-meta{padding:0 6px;font-size:.68rem;color:var(--mc-muted)}

/* composer: field-sizing grows the textarea, :has() dims Send while it is empty (required makes an empty field :invalid) */
.mc-form{display:flex;align-items:flex-end;gap:8px;padding:10px 12px 12px;border-top:1px solid var(--mc-line)}
.mc-form .form-control{--bs-border-color:#82958c;field-sizing:content;min-height:42px;max-height:132px;padding:10px 14px;border-width:1.5px;border-radius:14px;color:var(--mc-ink);font-size:inherit;line-height:1.4;resize:none}
.mc-form .form-control::placeholder{color:#5c6f66}
.mc-form .form-control:focus{border-color:var(--mc-accent);box-shadow:none}
.mc-form .form-control:focus-visible{outline-offset:1px}
.mc-send{--bs-btn-padding-x:11px;--bs-btn-padding-y:0;--bs-btn-font-weight:600;--bs-btn-font-size:.93rem;--bs-btn-border-width:0;--bs-btn-border-radius:14px;--bs-btn-color:#fff;--bs-btn-bg:var(--mc-accent);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--mc-accent-dark);--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--mc-accent-dark);--bs-btn-focus-box-shadow:none;
  flex:none;display:flex;align-items:center;justify-content:center;gap:6px;min-width:42px;height:42px;transition:background-color .2s,color .2s}
.mc-form:has(textarea:invalid) .mc-send{--bs-btn-bg:#dce5e1;--bs-btn-color:#4d6158;--bs-btn-hover-bg:#dce5e1;--bs-btn-hover-color:#4d6158}
.mc-label{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}

/* roomy layout when the slot is at least 420px wide */
@container mc (min-width:420px){
  .mc-head{padding:14px 20px}
  .mc-log{padding:18px 16px}
  .mc-msg{max-width:78%}
  .mc-av{display:grid}
  .mc-label{position:static;width:auto;height:auto;overflow:visible;clip:auto}
  .mc-send{--bs-btn-padding-x:16px;padding-left:18px}
  .mc-form{padding:12px 16px 16px}
}
@media (prefers-reduced-motion:reduce){.mc-msg,.mc-send{transition:none}}
JavaScript
/* The only script: append the message and handle Enter (Shift+Enter = new line). Growing, the Send state,
   staying scrolled to the newest message and the entrance animation are all CSS. Nothing leaves the page. */
document.querySelectorAll('[data-chat]').forEach(function (chat) {
  var form = chat.querySelector('[data-form]'), input = form.querySelector('textarea'), log = chat.querySelector('[data-log]');
  var pad = function (n) { return String(n).padStart(2, '0'); };
  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').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);
    input.value = ''; input.focus();
  });
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-mc-muted uppercase">In a slot 420px or wider: avatars and a labelled Send button</span>
<div class="@container/mc flex h-[500px] flex-col overflow-hidden rounded-[20px] border border-mc-line bg-white text-[.93rem] leading-[1.45] text-mc-ink shadow-[0_20px_44px_-28px_rgba(19,42,34,.4)]" data-chat>
  <div class="flex items-center gap-2.5 border-b border-mc-line px-3.5 py-3 @min-[420px]/mc:px-5 @min-[420px]/mc:py-3.5"><div><h2 class="text-[.98rem] font-bold" id="mc-title-a">Ridge Loop hike</h2><p class="text-[.76rem] text-mc-muted">Ana, Ben and you</p></div></div>
  <div class="flex flex-1 flex-col-reverse overflow-y-auto overscroll-contain px-2.5 py-3.5 [scrollbar-gutter:stable] focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-mc-accent @min-[420px]/mc:px-4 @min-[420px]/mc:py-[18px]" role="log" aria-live="polite" aria-labelledby="mc-title-a" tabindex="0">
    <div class="flex flex-col gap-2.5" data-log>
      <p class="self-center rounded-full bg-mc-in px-2.5 py-[3px] text-[.72rem] font-semibold text-mc-muted"><time datetime="2026-10-14">Today</time></p>
      <div class="flex max-w-[92%] items-end gap-2 transition-[opacity,translate] duration-300 ease-out starting:translate-y-2.5 starting:opacity-0 motion-reduce:transition-none @min-[420px]/mc:max-w-[78%]"><span class="hidden size-[30px] flex-none place-items-center rounded-full bg-[#fde68a] text-[.68rem] font-bold text-[#713f12] @min-[420px]/mc:grid" aria-hidden="true">AL</span><div class="flex min-w-0 flex-col items-start gap-0.5"><p class="ml-2.5 text-[.72rem] font-semibold text-mc-muted">Ana Lima</p><p class="rounded-[6px_16px_16px_16px] bg-mc-in px-3 py-[7px] text-pretty whitespace-pre-wrap [overflow-wrap:anywhere]">Forecast says dry until 15:00. Start at 07:30 from the north car park?</p><p class="px-1.5 text-[.68rem] text-mc-muted"><time datetime="2026-10-14T18:02">18:02</time></p></div></div>
      <div class="flex max-w-[92%] items-end gap-2 transition-[opacity,translate] duration-300 ease-out starting:translate-y-2.5 starting:opacity-0 motion-reduce:transition-none @min-[420px]/mc:max-w-[78%]"><span class="hidden size-[30px] flex-none place-items-center rounded-full bg-[#bfdbfe] text-[.68rem] font-bold text-[#1e3a8a] @min-[420px]/mc:grid" aria-hidden="true">BC</span><div class="flex min-w-0 flex-col items-start gap-0.5"><p class="ml-2.5 text-[.72rem] font-semibold text-mc-muted">Ben Cho</p><p class="rounded-[6px_16px_16px_16px] bg-mc-in px-3 py-[7px] text-pretty whitespace-pre-wrap [overflow-wrap:anywhere]">Works for me. I can bring the stove and coffee for the summit.</p><p class="px-1.5 text-[.68rem] text-mc-muted"><time datetime="2026-10-14T18:05">18:05</time></p></div></div>
      <div class="flex max-w-[92%] flex-row-reverse items-end gap-2 self-end transition-[opacity,translate] duration-300 ease-out starting:translate-y-2.5 starting:opacity-0 motion-reduce:transition-none @min-[420px]/mc:max-w-[78%]"><div class="flex min-w-0 flex-col items-end gap-0.5"><p class="sr-only">You</p><p class="rounded-[16px_6px_16px_16px] bg-mc-accent px-3 py-[7px] text-pretty whitespace-pre-wrap text-white [overflow-wrap:anywhere]">I'll print the map in case the signal drops on the east ridge.</p><p class="px-1.5 text-[.68rem] text-mc-muted"><time datetime="2026-10-14T18:07">18:07</time> · Read</p></div></div>
      <div class="flex max-w-[92%] items-end gap-2 transition-[opacity,translate] duration-300 ease-out starting:translate-y-2.5 starting:opacity-0 motion-reduce:transition-none @min-[420px]/mc:max-w-[78%]"><span class="hidden size-[30px] flex-none place-items-center rounded-full bg-[#fde68a] text-[.68rem] font-bold text-[#713f12] @min-[420px]/mc:grid" aria-hidden="true">AL</span><div class="flex min-w-0 flex-col items-start gap-0.5"><p class="ml-2.5 text-[.72rem] font-semibold text-mc-muted">Ana Lima</p><p class="rounded-[6px_16px_16px_16px] bg-mc-in px-3 py-[7px] text-pretty whitespace-pre-wrap [overflow-wrap:anywhere]">Perfect. 14 km and about 650 m of climbing in total.</p><p class="px-1.5 text-[.68rem] text-mc-muted"><time datetime="2026-10-14T18:09">18:09</time></p></div></div>
    </div>
  </div>
  <form class="group/f flex items-end gap-2 border-t border-mc-line px-3 pt-2.5 pb-3 @min-[420px]/mc:px-4 @min-[420px]/mc:pt-3 @min-[420px]/mc:pb-4" data-form>
    <label class="sr-only" for="mc-input-a">Message to Ridge Loop hike</label>
    <textarea class="box-border field-sizing-content min-h-[42px] max-h-[132px] min-w-0 flex-1 resize-none rounded-[14px] border-[1.5px] border-[#82958c] bg-white px-3.5 py-2.5 leading-[1.4] text-mc-ink placeholder:text-[#5c6f66] focus-visible:border-mc-accent focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-mc-accent" id="mc-input-a" rows="1" required placeholder="Write a message"></textarea>
    <button class="flex h-[42px] min-w-[42px] flex-none cursor-pointer items-center justify-center gap-1.5 rounded-[14px] bg-mc-accent px-[11px] font-semibold text-white transition-[background-color,color] duration-200 hover:bg-mc-accent-dark group-has-[textarea:invalid]/f:bg-[#dce5e1] group-has-[textarea:invalid]/f:text-mc-muted motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mc-accent @min-[420px]/mc:pr-4 @min-[420px]/mc:pl-[18px]" 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 @min-[420px]/mc:not-sr-only">Send</span></button>
  </form>
  <template><div class="flex max-w-[92%] flex-row-reverse items-end gap-2 self-end transition-[opacity,translate] duration-300 ease-out starting:translate-y-2.5 starting:opacity-0 motion-reduce:transition-none @min-[420px]/mc:max-w-[78%]"><div class="flex min-w-0 flex-col items-end gap-0.5"><p class="sr-only">You</p><p class="rounded-[16px_6px_16px_16px] bg-mc-accent px-3 py-[7px] text-pretty whitespace-pre-wrap text-white [overflow-wrap:anywhere]" data-text></p><p class="px-1.5 text-[.68rem] text-mc-muted"><time data-time></time> · Sent</p></div></div></template>
</div></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-mc-muted uppercase">In a 300px slot: the same markup, compact</span>
<div class="@container/mc flex h-[500px] flex-col overflow-hidden rounded-[20px] border border-mc-line bg-white text-[.93rem] leading-[1.45] text-mc-ink shadow-[0_20px_44px_-28px_rgba(19,42,34,.4)]" data-chat>
  <div class="flex items-center gap-2.5 border-b border-mc-line px-3.5 py-3 @min-[420px]/mc:px-5 @min-[420px]/mc:py-3.5"><div><h2 class="text-[.98rem] font-bold" id="mc-title-b">Ridge Loop hike</h2><p class="text-[.76rem] text-mc-muted">Ana, Ben and you</p></div></div>
  <div class="flex flex-1 flex-col-reverse overflow-y-auto overscroll-contain px-2.5 py-3.5 [scrollbar-gutter:stable] focus-visible:outline-2 focus-visible:-outline-offset-[3px] focus-visible:outline-mc-accent @min-[420px]/mc:px-4 @min-[420px]/mc:py-[18px]" role="log" aria-live="polite" aria-labelledby="mc-title-b" tabindex="0">
    <div class="flex flex-col gap-2.5" data-log>
      <p class="self-center rounded-full bg-mc-in px-2.5 py-[3px] text-[.72rem] font-semibold text-mc-muted"><time datetime="2026-10-14">Today</time></p>
      <div class="flex max-w-[92%] items-end gap-2 transition-[opacity,translate] duration-300 ease-out starting:translate-y-2.5 starting:opacity-0 motion-reduce:transition-none @min-[420px]/mc:max-w-[78%]"><span class="hidden size-[30px] flex-none place-items-center rounded-full bg-[#fde68a] text-[.68rem] font-bold text-[#713f12] @min-[420px]/mc:grid" aria-hidden="true">AL</span><div class="flex min-w-0 flex-col items-start gap-0.5"><p class="ml-2.5 text-[.72rem] font-semibold text-mc-muted">Ana Lima</p><p class="rounded-[6px_16px_16px_16px] bg-mc-in px-3 py-[7px] text-pretty whitespace-pre-wrap [overflow-wrap:anywhere]">Forecast says dry until 15:00. Start at 07:30 from the north car park?</p><p class="px-1.5 text-[.68rem] text-mc-muted"><time datetime="2026-10-14T18:02">18:02</time></p></div></div>
      <div class="flex max-w-[92%] items-end gap-2 transition-[opacity,translate] duration-300 ease-out starting:translate-y-2.5 starting:opacity-0 motion-reduce:transition-none @min-[420px]/mc:max-w-[78%]"><span class="hidden size-[30px] flex-none place-items-center rounded-full bg-[#bfdbfe] text-[.68rem] font-bold text-[#1e3a8a] @min-[420px]/mc:grid" aria-hidden="true">BC</span><div class="flex min-w-0 flex-col items-start gap-0.5"><p class="ml-2.5 text-[.72rem] font-semibold text-mc-muted">Ben Cho</p><p class="rounded-[6px_16px_16px_16px] bg-mc-in px-3 py-[7px] text-pretty whitespace-pre-wrap [overflow-wrap:anywhere]">Works for me. I can bring the stove and coffee for the summit.</p><p class="px-1.5 text-[.68rem] text-mc-muted"><time datetime="2026-10-14T18:05">18:05</time></p></div></div>
      <div class="flex max-w-[92%] flex-row-reverse items-end gap-2 self-end transition-[opacity,translate] duration-300 ease-out starting:translate-y-2.5 starting:opacity-0 motion-reduce:transition-none @min-[420px]/mc:max-w-[78%]"><div class="flex min-w-0 flex-col items-end gap-0.5"><p class="sr-only">You</p><p class="rounded-[16px_6px_16px_16px] bg-mc-accent px-3 py-[7px] text-pretty whitespace-pre-wrap text-white [overflow-wrap:anywhere]">I'll print the map in case the signal drops on the east ridge.</p><p class="px-1.5 text-[.68rem] text-mc-muted"><time datetime="2026-10-14T18:07">18:07</time> · Read</p></div></div>
      <div class="flex max-w-[92%] items-end gap-2 transition-[opacity,translate] duration-300 ease-out starting:translate-y-2.5 starting:opacity-0 motion-reduce:transition-none @min-[420px]/mc:max-w-[78%]"><span class="hidden size-[30px] flex-none place-items-center rounded-full bg-[#fde68a] text-[.68rem] font-bold text-[#713f12] @min-[420px]/mc:grid" aria-hidden="true">AL</span><div class="flex min-w-0 flex-col items-start gap-0.5"><p class="ml-2.5 text-[.72rem] font-semibold text-mc-muted">Ana Lima</p><p class="rounded-[6px_16px_16px_16px] bg-mc-in px-3 py-[7px] text-pretty whitespace-pre-wrap [overflow-wrap:anywhere]">Perfect. 14 km and about 650 m of climbing in total.</p><p class="px-1.5 text-[.68rem] text-mc-muted"><time datetime="2026-10-14T18:09">18:09</time></p></div></div>
    </div>
  </div>
  <form class="group/f flex items-end gap-2 border-t border-mc-line px-3 pt-2.5 pb-3 @min-[420px]/mc:px-4 @min-[420px]/mc:pt-3 @min-[420px]/mc:pb-4" data-form>
    <label class="sr-only" for="mc-input-b">Message to Ridge Loop hike</label>
    <textarea class="box-border field-sizing-content min-h-[42px] max-h-[132px] min-w-0 flex-1 resize-none rounded-[14px] border-[1.5px] border-[#82958c] bg-white px-3.5 py-2.5 leading-[1.4] text-mc-ink placeholder:text-[#5c6f66] focus-visible:border-mc-accent focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-mc-accent" id="mc-input-b" rows="1" required placeholder="Write a message"></textarea>
    <button class="flex h-[42px] min-w-[42px] flex-none cursor-pointer items-center justify-center gap-1.5 rounded-[14px] bg-mc-accent px-[11px] font-semibold text-white transition-[background-color,color] duration-200 hover:bg-mc-accent-dark group-has-[textarea:invalid]/f:bg-[#dce5e1] group-has-[textarea:invalid]/f:text-mc-muted motion-reduce:transition-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-mc-accent @min-[420px]/mc:pr-4 @min-[420px]/mc:pl-[18px]" 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 @min-[420px]/mc:not-sr-only">Send</span></button>
  </form>
  <template><div class="flex max-w-[92%] flex-row-reverse items-end gap-2 self-end transition-[opacity,translate] duration-300 ease-out starting:translate-y-2.5 starting:opacity-0 motion-reduce:transition-none @min-[420px]/mc:max-w-[78%]"><div class="flex min-w-0 flex-col items-end gap-0.5"><p class="sr-only">You</p><p class="rounded-[16px_6px_16px_16px] bg-mc-accent px-3 py-[7px] text-pretty whitespace-pre-wrap text-white [overflow-wrap:anywhere]" data-text></p><p class="px-1.5 text-[.68rem] text-mc-muted"><time data-time></time> · Sent</p></div></div></template>
</div></div>
CSS
/* Palette as theme colours. Container queries use @container/mc with @min-[420px]/mc: variants, the entrance uses starting:,
   the Send state uses group-has-[textarea:invalid]/f: and the textarea uses field-sizing-content. */
@theme {
  --color-mc-accent:#047857; --color-mc-accent-dark:#065f46; --color-mc-in:#eef3f1; --color-mc-ink:#132a22;
  --color-mc-muted:#4d6158; --color-mc-line:#d9e5df;
}
JavaScript
/* The only script: append the message and handle Enter (Shift+Enter = new line). Growing, the Send state,
   staying scrolled to the newest message and the entrance animation are all CSS. Nothing leaves the page. */
document.querySelectorAll('[data-chat]').forEach(function (chat) {
  var form = chat.querySelector('[data-form]'), input = form.querySelector('textarea'), log = chat.querySelector('[data-log]');
  var pad = function (n) { return String(n).padStart(2, '0'); };
  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').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);
    input.value = ''; input.focus();
  });
});

About this set

A chat that does with CSS what most chat widgets do with JavaScript, shown in a wide and a narrow slot. The log is a flex container with flex-direction: column-reverse around a normal message stack, so it opens at the newest message and stays there as new ones are added, with no scroll script. New messages fade and slide in through @starting-style. The textarea grows with its text through field-sizing: content, and because it is required, :has(textarea:invalid) dims the Send button while the field is empty. A container query on the chat itself switches layouts by the width of its slot rather than the window: from 420px up it shows avatars and a labelled Send button, below that it drops the avatars and keeps an icon button with a hidden label. The only script, about 20 lines, appends your message and handles Enter and Shift+Enter. The same markup fits a sidebar, a modal and a full page. Where field-sizing is unsupported the textarea keeps one row and scrolls, and where @starting-style is unsupported messages simply appear.

What’s included

  • column-reverse log that stays on the newest message
  • field-sizing: content textarea with no resize script
  • :has() dims Send while the required field is empty
  • Container query layout for wide and narrow slots
  • @starting-style entrance, removed under reduced motion

Accessibility

The log is a focusable role="log" region with aria-live="polite", and every message has a sender, hidden for your own, and a time element. Because the field is required, pressing Send while it is empty shows the browser's own validation message instead of failing silently. In the compact layout the Send label is visually hidden rather than removed, and the entrance transition is switched off under prefers-reduced-motion.

Customise it

Change --mc-accent, --mc-in and --mc-line on .mc, and the 420px value in @container mc (min-width: 420px) to choose where the roomy layout starts. In Tailwind edit the --color-mc-* theme colours and the @min-[420px]/mc: variants; in Bootstrap the same variables sit on the .card.