Build a two-pane inbox layout with Tailwind CSS. The left pane is a 22rem column with its own border: a header with "Inbox", an unread count pill and a filter glyph, a compact search field, and a scrollable list of conversation rows. Each row is a button carrying a data-thread id, and shows an unread dot, an avatar circle with initials, the sender in medium weight, the time right-aligned, the subject, and a one-line preview truncated with line-clamp. The right pane fills the rest: a header with the subject, participant line and action glyphs (archive, snooze, ⋯), a scrollable thread body of message bubbles alternating between grey incoming cards and a neutral-900 outgoing card, and a sticky reply box at the bottom with a textarea and a dark Send button. Then add JavaScript holding the thread contents in an object keyed by id: clicking a row moves the selected highlight, clears that row's unread dot and decrements the header count, renders the subject, participants and every message into the reading pane, and scrolls the pane back to the top of the thread.
Paste it into Claude, Cursor, v0 or any AI coding tool. Prefer the
finished markup? .
Build a two-pane inbox layout with Tailwind CSS. The left pane is a 22rem column with its own border: a header with "Inbox", an unread count pill and a filter glyph, a compact search field, and a scrollable list of conversation rows. Each row is a button carrying a data-thread id, and shows an unread dot, an avatar circle with initials, the sender in medium weight, the time right-aligned, the subject, and a one-line preview truncated with line-clamp. The right pane fills the rest: a header with the subject, participant line and action glyphs (archive, snooze, ⋯), a scrollable thread body of message bubbles alternating between grey incoming cards and a neutral-900 outgoing card, and a sticky reply box at the bottom with a textarea and a dark Send button. Then add JavaScript holding the thread contents in an object keyed by id: clicking a row moves the selected highlight, clears that row's unread dot and decrements the header count, renders the subject, participants and every message into the reading pane, and scrolls the pane back to the top of the thread.
var THREADS = {
ops: {
subject: "Runner capacity for the launch",
people: "Priya Kapoor, you and 2 others",
messages: [
{ from: "Priya Kapoor", initials: "PK", time: "09:12", mine: false, text: "We are at 78% on the shared pool and the marketing site rebuild lands Thursday. Can we pin two dedicated runners for launch week?" },
{ from: "You", initials: "SM", time: "09:20", mine: true, text: "Yes — I'll reserve two and set the pool to scale at 70% instead of 85%." },
{ from: "Priya Kapoor", initials: "PK", time: "09:26", mine: false, text: "Perfect. I'll tell the web team they can stop staggering their deploys." }
]
},
billing: {
subject: "Annual invoice — PO number needed",
people: "Finance and you",
messages: [
{ from: "Finance", initials: "FN", time: "08:40", mine: false, text: "Procurement will not release payment without the PO on the invoice header. Can you have it reissued as PO-44192?" },
{ from: "You", initials: "SM", time: "08:52", mine: true, text: "Requested — the reissued PDF should land in your inbox within the hour." }
]
},
incident: {
subject: "Postmortem draft: cache stampede",
people: "Jonas Ohlsson, you and 5 others",
messages: [
{ from: "Jonas Ohlsson", initials: "JO", time: "Yesterday", mine: false, text: "Draft is ready for review. Two action items are still unassigned: the jittered retry and the alert on cache hit rate." },
{ from: "You", initials: "SM", time: "Yesterday", mine: true, text: "I'll take the retry work. Can Mei own the alert since she rebuilt that dashboard?" },
{ from: "Jonas Ohlsson", initials: "JO", time: "Yesterday", mine: false, text: "Asked her — she is in. Publishing Friday unless anyone objects." }
]
},
design: {
subject: "New status colours in the design kit",
people: "Mei Tanaka and you",
messages: [
{ from: "Mei Tanaka", initials: "MT", time: "Mon", mine: false, text: "Amber now means degraded rather than warning, so the legend changes too. Tokens are published under status/*." }
]
}
};
var rows = [].slice.call(document.querySelectorAll("[data-thread]"));
var subject = document.getElementById("threadSubject");
var people = document.getElementById("threadPeople");
var body = document.getElementById("threadBody");
var unreadCount = document.getElementById("unreadCount");
var SELECTED = "flex w-full gap-3 bg-neutral-100 px-4 py-3 text-left";
var IDLE = "flex w-full gap-3 px-4 py-3 text-left transition hover:bg-neutral-50";
function bubble(message) {
var wrap = document.createElement("div");
wrap.className = "flex gap-3 " + (message.mine ? "flex-row-reverse" : "");
var avatar = document.createElement("span");
avatar.className = "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-[11px] font-medium " +
(message.mine ? "bg-neutral-900 text-white" : "bg-neutral-200 text-neutral-700");
avatar.textContent = message.initials;
var card = document.createElement("div");
card.className = "max-w-[80%] rounded-2xl px-4 py-3 text-sm leading-relaxed " +
(message.mine ? "bg-neutral-900 text-neutral-100" : "bg-neutral-50 text-neutral-700");
card.innerHTML = '<p class="mb-1 text-xs ' + (message.mine ? "text-neutral-500" : "text-neutral-400") + '">' +
message.from + " · " + message.time + "</p>";
card.appendChild(document.createTextNode(message.text));
wrap.appendChild(avatar);
wrap.appendChild(card);
return wrap;
}
function open(row) {
var thread = THREADS[row.getAttribute("data-thread")];
rows.forEach(function (other) { other.className = other === row ? SELECTED : IDLE; });
// Opening a conversation is what marks it read.
var dot = row.querySelector("[data-unread]");
if (!dot.classList.contains("hidden")) {
dot.classList.add("hidden");
unreadCount.textContent = Math.max(0, Number(unreadCount.textContent) - 1);
unreadCount.classList.toggle("hidden", unreadCount.textContent === "0");
}
subject.textContent = thread.subject;
people.textContent = thread.people;
body.innerHTML = "";
thread.messages.forEach(function (message) { body.appendChild(bubble(message)); });
body.scrollTop = 0;
}
rows.forEach(function (row) {
row.addEventListener("click", function () { open(row); });
});
open(rows[0]);