Bootstrap 5 Chat Window with Conversation List
A two-pane chat layout with a searchable conversation list and a message thread with avatars and timestamps. Switching conversations, filtering and sending messages all work.
<section class="wdw-stage">
<div class="container" style="max-width: 1000px">
<div class="wdw-card overflow-hidden chat" id="chat">
<div class="row g-0">
<aside class="col-md-4 border-end chat-side">
<div class="p-3 pb-2">
<div class="d-flex align-items-center justify-content-between mb-3">
<h2 class="h5 fw-bold mb-0 text-body-emphasis">Messages</h2>
<span class="badge rounded-pill bg-primary-subtle text-primary-emphasis" id="chat-new">3 new</span>
</div>
<label for="chat-search" class="visually-hidden">Search conversations</label>
<input type="search" class="form-control bg-body-tertiary border-0" id="chat-search" placeholder="Search conversations">
</div>
<ul class="list-unstyled mb-0 px-2 pb-2 chat-people" id="chat-people">
<li>
<button type="button" class="chat-person is-active w-100 d-flex align-items-center gap-3 text-start" aria-current="true" data-chat="0" data-name="Maya Lindqvist" data-img="47">
<img src="https://i.pravatar.cc/80?img=47" width="44" height="44" class="rounded-circle flex-shrink-0" alt="">
<span class="flex-grow-1 min-w-0">
<span class="d-flex justify-content-between gap-2"><span class="fw-semibold text-body-emphasis text-truncate">Maya Lindqvist</span><span class="small wdw-muted flex-shrink-0">Dec 25</span></span>
<span class="d-flex justify-content-between gap-2 align-items-center"><span class="small wdw-muted text-truncate">Sounds good, I will send the files tonight.</span><span class="badge rounded-pill bg-primary chat-unread" data-n="2">2</span></span>
</span>
</button>
</li>
<li>
<button type="button" class="chat-person w-100 d-flex align-items-center gap-3 text-start" data-chat="1" data-name="Daniel Okafor" data-img="12">
<img src="https://i.pravatar.cc/80?img=12" width="44" height="44" class="rounded-circle flex-shrink-0" alt="">
<span class="flex-grow-1 min-w-0">
<span class="d-flex justify-content-between gap-2"><span class="fw-semibold text-body-emphasis text-truncate">Daniel Okafor</span><span class="small wdw-muted flex-shrink-0">Dec 24</span></span>
<span class="d-flex justify-content-between gap-2 align-items-center"><span class="small wdw-muted text-truncate">Can we move the review to Thursday?</span></span>
</span>
</button>
</li>
<li>
<button type="button" class="chat-person w-100 d-flex align-items-center gap-3 text-start" data-chat="2" data-name="Sofia Marin" data-img="32">
<img src="https://i.pravatar.cc/80?img=32" width="44" height="44" class="rounded-circle flex-shrink-0" alt="">
<span class="flex-grow-1 min-w-0">
<span class="d-flex justify-content-between gap-2"><span class="fw-semibold text-body-emphasis text-truncate">Sofia Marin</span><span class="small wdw-muted flex-shrink-0">Dec 22</span></span>
<span class="d-flex justify-content-between gap-2 align-items-center"><span class="small wdw-muted text-truncate">The new mockups look great!</span></span>
</span>
</button>
</li>
<li>
<button type="button" class="chat-person w-100 d-flex align-items-center gap-3 text-start" data-chat="3" data-name="Tomas Vidal" data-img="15">
<img src="https://i.pravatar.cc/80?img=15" width="44" height="44" class="rounded-circle flex-shrink-0" alt="">
<span class="flex-grow-1 min-w-0">
<span class="d-flex justify-content-between gap-2"><span class="fw-semibold text-body-emphasis text-truncate">Tomas Vidal</span><span class="small wdw-muted flex-shrink-0">Dec 20</span></span>
<span class="d-flex justify-content-between gap-2 align-items-center"><span class="small wdw-muted text-truncate">Thanks, that fixed the build.</span></span>
</span>
</button>
</li>
<li>
<button type="button" class="chat-person w-100 d-flex align-items-center gap-3 text-start" data-chat="4" data-name="Priya Nair" data-img="45">
<img src="https://i.pravatar.cc/80?img=45" width="44" height="44" class="rounded-circle flex-shrink-0" alt="">
<span class="flex-grow-1 min-w-0">
<span class="d-flex justify-content-between gap-2"><span class="fw-semibold text-body-emphasis text-truncate">Priya Nair</span><span class="small wdw-muted flex-shrink-0">Dec 18</span></span>
<span class="d-flex justify-content-between gap-2 align-items-center"><span class="small wdw-muted text-truncate">Let me know when you are free to chat.</span><span class="badge rounded-pill bg-primary chat-unread" data-n="1">1</span></span>
</span>
</button>
</li>
<li>
<button type="button" class="chat-person w-100 d-flex align-items-center gap-3 text-start" data-chat="5" data-name="Brian Hale" data-img="53">
<img src="https://i.pravatar.cc/80?img=53" width="44" height="44" class="rounded-circle flex-shrink-0" alt="">
<span class="flex-grow-1 min-w-0">
<span class="d-flex justify-content-between gap-2"><span class="fw-semibold text-body-emphasis text-truncate">Brian Hale</span><span class="small wdw-muted flex-shrink-0">Dec 15</span></span>
<span class="d-flex justify-content-between gap-2 align-items-center"><span class="small wdw-muted text-truncate">Invoice attached for December.</span></span>
</span>
</button>
</li>
</ul>
</aside>
<section class="col-md-8 d-flex flex-column chat-main" aria-label="Conversation">
<header class="d-flex align-items-center gap-3 px-4 py-3 border-bottom">
<img src="https://i.pravatar.cc/80?img=47" width="40" height="40" class="rounded-circle" alt="" id="chat-avatar">
<div><div class="fw-semibold text-body-emphasis" id="chat-title">Maya Lindqvist</div><div class="small text-success-emphasis d-flex align-items-center gap-1"><span class="chat-dot" aria-hidden="true"></span>Online</div></div>
</header>
<div class="chat-thread flex-grow-1 p-4 d-flex flex-column gap-3" id="chat-thread" aria-live="polite">
<div class="chat-msg in"><img src="https://i.pravatar.cc/80?img=47" width="36" height="36" class="rounded-circle" alt=""><div><div class="chat-bubble">Hi! Did you get a chance to look at the proposal?</div><div class="chat-time">11:01 AM | June 9</div></div></div>
<div class="chat-msg out"><div><div class="chat-bubble">Yes, it looks solid. A few small notes on the timeline.</div><div class="chat-time">11:04 AM | June 9</div></div></div>
<div class="chat-msg in"><img src="https://i.pravatar.cc/80?img=47" width="36" height="36" class="rounded-circle" alt=""><div><div class="chat-bubble">Great, we work directly with our designers and suppliers, so we can adjust quickly.</div><div class="chat-time">11:06 AM | Yesterday</div></div></div>
<div class="chat-msg out"><div><div class="chat-bubble">Perfect. Send me the updated version when ready.</div><div class="chat-time">11:09 AM | Today</div></div></div>
<div class="chat-msg in"><img src="https://i.pravatar.cc/80?img=47" width="36" height="36" class="rounded-circle" alt=""><div><div class="chat-bubble">Sounds good, I will send the files tonight.</div><div class="chat-time">11:12 AM | Today</div></div></div>
</div>
<form class="d-flex gap-2 p-3 border-top" id="chat-form">
<label for="chat-input" class="visually-hidden">Type a message</label>
<input type="text" class="form-control rounded-pill bg-body-tertiary border-0 px-4" id="chat-input" placeholder="Type a message" autocomplete="off">
<button type="submit" class="btn btn-primary rounded-circle p-0 flex-shrink-0" style="width:44px" aria-label="Send message"><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="M22 2 11 13M22 2l-7 20-4-9-9-4z"/></svg></button>
</form>
</section>
</div>
</div>
</div>
</section>
/* Web Designer Wall library style — shared base for every Bootstrap 5 snippet.
Indigo accent, Plus Jakarta Sans, soft cards, pill buttons. Works in light and dark. */
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap");
:root,
[data-bs-theme="light"] {
--wdw-font: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--bs-body-font-family: var(--wdw-font);
--bs-primary: #4f46e5;
--bs-primary-rgb: 79, 70, 229;
--bs-primary-text-emphasis: #3730a3;
--bs-primary-bg-subtle: #eef2ff;
--bs-primary-border-subtle: #c7d2fe;
--bs-link-color: #4f46e5;
--bs-link-color-rgb: 79, 70, 229;
--bs-link-hover-color: #4338ca;
--bs-link-hover-color-rgb: 67, 56, 202;
--bs-focus-ring-color: rgba(79, 70, 229, 0.25);
--bs-border-radius: 0.75rem;
--bs-border-radius-lg: 1rem;
--bs-border-radius-xl: 1.25rem;
--wdw-radius-card: 1.25rem;
--wdw-shadow: 0 1px 2px rgba(16, 24, 40, 0.05), 0 16px 40px -16px rgba(16, 24, 40, 0.22);
--wdw-shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06), 0 4px 12px -4px rgba(16, 24, 40, 0.12);
--wdw-stage: #f5f6fb;
}
[data-bs-theme="dark"] {
--bs-primary: #818cf8;
--bs-primary-rgb: 129, 140, 248;
--bs-primary-text-emphasis: #c7d2fe;
--bs-primary-bg-subtle: rgba(129, 140, 248, 0.14);
--bs-primary-border-subtle: rgba(129, 140, 248, 0.35);
--bs-link-color: #a5b4fc;
--bs-link-color-rgb: 165, 180, 252;
--bs-link-hover-color: #c7d2fe;
--bs-link-hover-color-rgb: 199, 210, 254;
--bs-focus-ring-color: rgba(129, 140, 248, 0.35);
--wdw-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 16px 40px -16px rgba(0, 0, 0, 0.7);
--wdw-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), 0 4px 12px -4px rgba(0, 0, 0, 0.5);
--wdw-stage: #0f1117;
}
body { font-family: var(--wdw-font); -webkit-font-smoothing: antialiased; }
/* Bootstrap compiles its blue into these components; point them at the accent. */
.btn-primary {
--bs-btn-bg: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: #4338ca; --bs-btn-hover-border-color: #4338ca;
--bs-btn-active-bg: #3730a3; --bs-btn-active-border-color: #3730a3;
--bs-btn-disabled-bg: var(--bs-primary); --bs-btn-disabled-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
[data-bs-theme="dark"] .btn-primary { --bs-btn-color: #0f1117; --bs-btn-hover-bg: #a5b4fc; --bs-btn-hover-border-color: #a5b4fc; --bs-btn-hover-color: #0f1117; --bs-btn-active-color: #0f1117; }
.btn-outline-primary {
--bs-btn-color: var(--bs-primary); --bs-btn-border-color: var(--bs-primary);
--bs-btn-hover-bg: var(--bs-primary); --bs-btn-hover-border-color: var(--bs-primary);
--bs-btn-active-bg: var(--bs-primary); --bs-btn-active-border-color: var(--bs-primary);
--bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}
.btn { --bs-btn-font-weight: 600; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; gap: .5rem; }
.btn-sm { min-height: 36px; }
.form-control, .form-select { min-height: 44px; border-radius: .75rem; }
.form-control:focus, .form-select:focus, .form-check-input:focus { border-color: rgba(var(--bs-primary-rgb), .6); box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.form-check-input:checked { background-color: var(--bs-primary); border-color: var(--bs-primary); }
.nav-pills { --bs-nav-pills-link-active-bg: var(--bs-primary); }
.nav { --bs-nav-link-color: var(--bs-secondary-color); --bs-nav-link-hover-color: var(--bs-emphasis-color); }
.pagination { --bs-pagination-active-bg: var(--bs-primary); --bs-pagination-active-border-color: var(--bs-primary); --bs-pagination-color: var(--bs-body-color); --bs-pagination-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.progress, .progress-stacked { --bs-progress-bar-bg: var(--bs-primary); }
.list-group { --bs-list-group-active-bg: var(--bs-primary); --bs-list-group-active-border-color: var(--bs-primary); }
.dropdown-menu { --bs-dropdown-link-active-bg: var(--bs-primary); border-radius: .875rem; box-shadow: var(--wdw-shadow-sm); }
.accordion { --bs-accordion-active-bg: var(--bs-primary-bg-subtle); --bs-accordion-active-color: var(--bs-primary-text-emphasis); --bs-accordion-btn-focus-box-shadow: 0 0 0 .25rem var(--bs-focus-ring-color); }
.badge { font-weight: 600; }
/* Library building blocks — use these instead of re-inventing them per snippet. */
.wdw-stage { min-height: 100vh; display: flex; align-items: center; background: var(--wdw-stage); padding-block: 3rem; }
.wdw-card { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: var(--wdw-radius-card); box-shadow: var(--wdw-shadow); }
.wdw-card-sm { background: var(--bs-body-bg); border: 1px solid var(--bs-border-color-translucent); border-radius: 1rem; box-shadow: var(--wdw-shadow-sm); }
.wdw-aurora { position: relative; color: #fff; background: radial-gradient(60% 55% at 100% 0, #818cf8 0, transparent 70%), radial-gradient(65% 60% at 0 100%, #ec4899 0, transparent 65%), #1e1b4b; }
.wdw-eyebrow { display: inline-block; font-size: .75rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--bs-primary); }
.wdw-heading { font-weight: 800; letter-spacing: -.02em; color: var(--bs-emphasis-color); }
.wdw-muted { color: var(--bs-secondary-color); }
.wdw-icon { width: 2.75rem; height: 2.75rem; flex: none; display: inline-flex; align-items: center; justify-content: center; border-radius: .875rem; background: var(--bs-primary-bg-subtle); color: var(--bs-primary); }
/* Component */
.chat { height: min(640px, calc(100vh - 6rem)); min-height: 520px; }
.chat > .row { height: 100%; }
.chat-side { display: flex; flex-direction: column; min-height: 0; }
.chat-people { overflow-y: auto; min-height: 0; flex: 1; }
.chat-main { min-height: 0; }
.chat-person { border: 0; background: transparent; color: inherit; padding: .7rem .75rem; border-radius: .875rem; transition: background-color .15s ease; }
.chat-person:hover { background: var(--bs-tertiary-bg); }
.chat-person.is-active { background: var(--bs-primary-bg-subtle); }
.chat-person:focus-visible { outline: 2px solid var(--bs-primary); outline-offset: -2px; }
.chat-dot { width: .5rem; height: .5rem; border-radius: 50%; background: var(--bs-success); }
.chat-thread { overflow-y: auto; min-height: 0; }
.chat-msg { display: flex; gap: .75rem; align-items: flex-end; max-width: 80%; }
.chat-msg.out { align-self: flex-end; }
.chat-bubble { padding: .65rem 1rem; font-size: .9rem; line-height: 1.45; border-radius: 1.1rem 1.1rem 1.1rem .3rem; background: var(--bs-tertiary-bg); color: var(--bs-body-color); }
.chat-msg.out .chat-bubble { background: var(--bs-primary); color: #fff; border-radius: 1.1rem 1.1rem .3rem 1.1rem; }
[data-bs-theme="dark"] .chat-msg.out .chat-bubble { color: #0f1117; }
.chat-time { font-size: .72rem; color: var(--bs-secondary-color); margin-top: .3rem; }
.chat-msg.out .chat-time { text-align: right; }
@media (max-width: 767.98px) {
.chat { height: auto; }
.chat-people { display: flex; overflow-x: auto; overflow-y: hidden; gap: .25rem; flex: none; }
.chat-people > li { flex: 0 0 15rem; width: 15rem; min-width: 0; }
.chat-side { flex: none; }
.chat-side { border-right: 0 !important; border-bottom: 1px solid var(--bs-border-color); }
.chat-thread { max-height: 24rem; }
.chat-msg { max-width: 92%; }
}
.min-w-0 { min-width: 0; }
(function () {
function initChat() {
var root = document.getElementById('chat');
if (!root) return;
var people = root.querySelectorAll('.chat-person');
var thread = document.getElementById('chat-thread');
var title = document.getElementById('chat-title');
var avatar = document.getElementById('chat-avatar');
var form = document.getElementById('chat-form');
var input = document.getElementById('chat-input');
var search = document.getElementById('chat-search');
var newBadge = document.getElementById('chat-new');
function time() {
return new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }) + ' | Today';
}
function addMessage(text, out, stampText) {
var wrap = document.createElement('div');
wrap.className = 'chat-msg ' + (out ? 'out' : 'in');
var body = document.createElement('div');
var bubble = document.createElement('div');
bubble.className = 'chat-bubble';
bubble.textContent = text;
var stamp = document.createElement('div');
stamp.className = 'chat-time';
stamp.textContent = stampText || time();
body.append(bubble, stamp);
if (!out) {
var img = document.createElement('img');
img.src = avatar.src; img.width = 36; img.height = 36; img.alt = '';
img.className = 'rounded-circle';
wrap.appendChild(img);
}
wrap.appendChild(body);
thread.appendChild(wrap);
thread.scrollTop = thread.scrollHeight;
}
var threads = {
0: [[0,'Hi! Did you get a chance to look at the proposal?','11:01 AM | June 9'],[1,'Yes, it looks solid. A few small notes on the timeline.','11:04 AM | June 9'],[0,'Great, we work directly with our designers and suppliers, so we can adjust quickly.','11:06 AM | Yesterday'],[1,'Perfect. Send me the updated version when ready.','11:09 AM | Today'],[0,'Sounds good, I will send the files tonight.','11:12 AM | Today']],
1: [[0,'Can we move the review to Thursday?','9:40 AM | Dec 24'],[1,'Thursday works. Afternoon is better for me.','9:52 AM | Dec 24'],[0,'Great, 3 PM then. I will send an invite.','9:55 AM | Dec 24']],
2: [[1,'Here are the new mockups for the landing page.','4:10 PM | Dec 22'],[0,'The new mockups look great!','4:31 PM | Dec 22'],[1,'Glad you like them. I will export the assets next.','4:35 PM | Dec 22']],
3: [[0,'The build is failing on the staging branch.','2:02 PM | Dec 20'],[1,'Try clearing the cache and reinstalling dependencies.','2:15 PM | Dec 20'],[0,'Thanks, that fixed the build.','2:40 PM | Dec 20']],
4: [[0,'Hey, do you have a minute this week?','10:20 AM | Dec 18'],[1,'Sure, what is it about?','10:48 AM | Dec 18'],[0,'Let me know when you are free to chat.','11:05 AM | Dec 18']],
5: [[0,'Invoice attached for December.','8:30 AM | Dec 15'],[1,'Received, I will process it today.','9:12 AM | Dec 15'],[0,'Thank you, much appreciated.','9:14 AM | Dec 15']]
};
function render(id) {
thread.textContent = '';
(threads[id] || []).forEach(function (m) { addMessage(m[1], m[0] === 1, m[2]); });
}
function refreshNew() {
var n = 0;
root.querySelectorAll('.chat-unread').forEach(function (b) { n += parseInt(b.dataset.n, 10) || 0; });
newBadge.textContent = n ? n + ' new' : 'All read';
}
people.forEach(function (p) {
p.addEventListener('click', function () {
people.forEach(function (o) { o.classList.remove('is-active'); o.removeAttribute('aria-current'); });
p.classList.add('is-active');
p.setAttribute('aria-current', 'true');
var unread = p.querySelector('.chat-unread');
if (unread) { unread.remove(); refreshNew(); }
title.textContent = p.dataset.name;
avatar.src = 'https://i.pravatar.cc/80?img=' + p.dataset.img;
render(p.dataset.chat);
});
});
form.addEventListener('submit', function (e) {
e.preventDefault();
var text = input.value.trim();
if (!text) return;
addMessage(text, true);
input.value = '';
setTimeout(function () { addMessage('Got it, thanks!', false); }, 900);
});
search.addEventListener('input', function () {
var q = search.value.trim().toLowerCase();
people.forEach(function (p) {
p.parentElement.hidden = q && p.dataset.name.toLowerCase().indexOf(q) === -1;
});
});
}
initChat();
})();
Built on Bootstrap 5.3.8 with plain JavaScript — no jQuery. Supports light and dark mode via data-bs-theme.