Social post
A feed post: the author's photo with a follow button, a photo, like, comment, share and save, a like count that changes, a caption that opens with "more" and the top comment.
Today 09:41
Are you still up for the flea market on Saturday?
The ceramics stall opens at 8 and the good bowls go fast
Replying to Ximena The ceramics stall opens at 8 and the good bowls go fast
8 is brutal but fine. I'll bring coffee
Meet by the fountain?
Perfect. Oat milk for me please
You reacted with a heartNoted. See you at 7:50
Seen 09:52
It is live: hover, focus and its own controls respond. Actions that would change data (approve, send, delete) are for your app to wire. The width is a real 390 px, 768 px or 1280 px box, shrunk to fit your screen.
<!-- Icons: Lucide (lucide.dev), ISC licence -->
<style>
.odl-chat-thread { box-sizing: border-box; inline-size: 100%; max-inline-size: 32rem; margin-inline: auto; container-type: inline-size; color: var(--ds-color-text-primary); font: var(--ds-text-body-md); text-wrap: pretty; }
.odl-chat-thread *, .odl-chat-thread *::before, .odl-chat-thread *::after { box-sizing: border-box; }
.odl-chat-thread .odl-card { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr); max-block-size: 46rem; background: var(--ds-color-surface-raised);
border: var(--ds-border-width-default) solid var(--ds-color-border-subtle); border-radius: var(--ds-radius-container); box-shadow: var(--ds-elevation-raised); overflow: hidden; }
.odl-chat-thread button { display: inline-grid; place-items: center; flex: none; min-inline-size: var(--ds-size-target-touch); min-block-size: var(--ds-size-target-touch); padding: 0; border: 0; border-radius: var(--ds-radius-full);
background: transparent; color: var(--ds-color-text-primary); cursor: pointer; transition: background-color var(--ds-motion-duration-short) var(--ds-motion-easing-standard); }
.odl-chat-thread button:hover:not(:disabled) { background: var(--ds-color-bg-neutral-subtle); }
.odl-chat-thread button svg { inline-size: var(--ds-size-icon-md); block-size: var(--ds-size-icon-md); stroke-width: var(--ds-icon-stroke-md); }
.odl-chat-thread .odl-head { display: flex; align-items: center; gap: var(--ds-space-inline-xs); padding: var(--ds-space-inset-sm) var(--ds-space-inset-sm); border-block-end: var(--ds-border-width-default) solid var(--ds-color-border-subtle); }
.odl-chat-thread .odl-who { display: flex; align-items: center; gap: var(--ds-space-inline-md); flex: 1; min-inline-size: 0; }
.odl-chat-thread .odl-who > span:last-child { display: grid; gap: var(--ds-space-stack-xs); min-inline-size: 0; }
.odl-chat-thread .odl-who b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: var(--ds-text-label-lg); font-weight: var(--ds-font-weight-heading); }
.odl-chat-thread .odl-who small { color: var(--ds-color-text-secondary); font: var(--ds-text-label-md); }
.odl-chat-thread .odl-av { position: relative; display: grid; flex: none; inline-size: var(--ds-size-control-lg); aspect-ratio: 1; border-radius: var(--ds-radius-person); }
.odl-chat-thread .odl-av img { display: block; inline-size: 100%; block-size: 100%; border-radius: var(--ds-radius-person); object-fit: cover; background: var(--ds-color-bg-neutral-subtle); }
.odl-chat-thread .odl-av.is-sm { inline-size: var(--ds-size-control-sm); }
.odl-chat-thread .odl-on { position: absolute; inset-inline-end: 0; inset-block-end: 0; inline-size: 28%; aspect-ratio: 1; border-radius: var(--ds-radius-full); background: var(--ds-color-bg-success-bold);
border: var(--ds-border-width-emphasis) solid var(--ds-color-surface-raised); }
.odl-chat-thread .odl-log { display: flex; flex-direction: column; gap: var(--ds-space-4); padding: var(--ds-space-inset-lg) var(--ds-space-inset-xl); overflow-y: auto; overscroll-behavior: contain; background: var(--ds-color-surface-raised); }
.odl-chat-thread .odl-day { align-self: center; margin: 0 0 var(--ds-space-stack-md); color: var(--ds-color-text-tertiary); font: var(--ds-text-label-sm); font-variant-numeric: tabular-nums; }
.odl-chat-thread .odl-row { display: flex; align-items: flex-end; gap: var(--ds-space-inline-sm); }
.odl-chat-thread .odl-row.is-gap { margin-block-start: var(--ds-space-stack-md); }
.odl-chat-thread .odl-row.is-in .odl-slot { flex: none; inline-size: var(--ds-size-control-sm); }
.odl-chat-thread .odl-row.is-out { justify-content: flex-end; }
.odl-chat-thread .odl-stack { position: relative; display: grid; justify-items: start; max-inline-size: min(78%, 22rem); }
.odl-chat-thread .is-out .odl-stack { justify-items: end; }
/* Their bubbles are grey; yours take the accent's soft tint, so the accent stays for Send. */
.odl-chat-thread .odl-msg { position: relative; margin: 0; padding: var(--ds-space-stack-sm) var(--ds-space-inset-md); border-radius: var(--ds-radius-container); overflow-wrap: anywhere; }
.odl-chat-thread .is-in .odl-msg { background: var(--ds-color-bg-neutral-subtle); color: var(--ds-color-text-primary); }
.odl-chat-thread .is-out .odl-msg { background: var(--ds-color-bg-accent-subtle); color: var(--ds-color-text-primary); }
/* Bubbles in a run share a flatter corner on the sender's side. */
.odl-chat-thread .is-in .odl-msg.is-first { border-end-start-radius: var(--ds-radius-detail); }
.odl-chat-thread .is-in .odl-msg.is-mid { border-start-start-radius: var(--ds-radius-detail); border-end-start-radius: var(--ds-radius-detail); }
.odl-chat-thread .is-in .odl-msg.is-last { border-start-start-radius: var(--ds-radius-detail); }
.odl-chat-thread .is-out .odl-msg.is-first { border-end-end-radius: var(--ds-radius-detail); }
.odl-chat-thread .is-out .odl-msg.is-last { border-start-end-radius: var(--ds-radius-detail); }
.odl-chat-thread .odl-msg p { margin: 0; }
.odl-chat-thread .odl-msg .odl-quote { display: grid; gap: var(--ds-space-2); margin: 0 calc(-1 * var(--ds-space-4)) var(--ds-space-stack-sm); padding: var(--ds-space-stack-sm) var(--ds-space-inset-sm);
border-radius: max(var(--ds-radius-detail), calc(var(--ds-radius-container) - var(--ds-space-8))); background: var(--ds-color-surface-raised); border-inline-start: var(--ds-border-width-emphasis) solid var(--ds-color-border-accent);
color: var(--ds-color-text-secondary); font: var(--ds-text-body-sm); }
.odl-chat-thread .odl-quote b { color: var(--ds-color-text-primary); font: var(--ds-text-label-md); font-weight: var(--ds-font-weight-heading); }
.odl-chat-thread .odl-msg.has-react { margin-block-end: var(--ds-space-16); }
.odl-chat-thread .odl-react { position: absolute; inset-block-end: calc(-1 * var(--ds-space-4)); inset-inline-end: var(--ds-space-8); display: inline-flex; align-items: center; gap: var(--ds-space-2);
padding: var(--ds-space-4) var(--ds-space-6); border-radius: var(--ds-radius-full); background: var(--ds-color-surface-raised); border: var(--ds-border-width-default) solid var(--ds-color-border-subtle);
box-shadow: var(--ds-elevation-raised); color: var(--ds-color-text-primary); font: var(--ds-text-label-sm); }
.odl-chat-thread .odl-react svg { inline-size: var(--ds-size-icon-sm); block-size: var(--ds-size-icon-sm); fill: currentColor; stroke-width: var(--ds-icon-stroke-sm); }
.odl-chat-thread .odl-seen { margin: var(--ds-space-stack-xs) 0 0; color: var(--ds-color-text-tertiary); font: var(--ds-text-label-sm); font-variant-numeric: tabular-nums; }
.odl-chat-thread .odl-typing { display: inline-flex; gap: var(--ds-space-4); padding: var(--ds-space-12) var(--ds-space-16); border-radius: var(--ds-radius-container); border-end-start-radius: var(--ds-radius-detail); background: var(--ds-color-bg-neutral-subtle); }
.odl-chat-thread .odl-typing i { inline-size: var(--ds-space-6); block-size: var(--ds-space-6); border-radius: var(--ds-radius-full); background: var(--ds-color-icon-subtle);
animation: odl-chat-thread-dot var(--ds-motion-duration-extra) var(--ds-motion-easing-standard) infinite; }
.odl-chat-thread .odl-typing i:nth-child(2) { animation-delay: var(--ds-motion-duration-short); }
.odl-chat-thread .odl-typing i:nth-child(3) { animation-delay: var(--ds-motion-duration-medium); }
@keyframes odl-chat-thread-dot { 0%, 60%, 100% { transform: translateY(0); opacity: 0.5; } 30% { transform: translateY(-40%); opacity: 1; } }
.odl-chat-thread .odl-composer { display: flex; align-items: center; gap: var(--ds-space-inline-sm); margin: 0; padding: var(--ds-space-inset-sm) var(--ds-space-inset-md);
border-block-start: var(--ds-border-width-default) solid var(--ds-color-border-subtle); background: var(--ds-color-surface-raised); }
.odl-chat-thread .odl-composer input { flex: 1; min-inline-size: 0; min-block-size: var(--ds-size-target-touch); padding: 0 var(--ds-space-inset-md); border: var(--ds-border-width-default) solid var(--ds-color-border-input);
border-radius: var(--ds-radius-control); background: var(--ds-color-bg-field); color: var(--ds-color-text-primary); font: var(--ds-text-body-md); }
.odl-chat-thread .odl-composer input::placeholder { color: var(--ds-color-text-tertiary); }
.odl-chat-thread .odl-send { background: var(--ds-color-bg-accent-bold); color: var(--ds-color-text-on-accent); }
.odl-chat-thread .odl-send:hover:not(:disabled) { background: var(--ds-color-bg-accent-bold-hover); }
.odl-chat-thread .odl-send:disabled { background: var(--ds-color-bg-disabled); color: var(--ds-color-text-disabled); cursor: default; }
.odl-chat-thread button:focus-visible, .odl-chat-thread input:focus-visible { outline: var(--ds-focus-ring-width) solid var(--ds-color-border-focus); outline-offset: var(--ds-focus-ring-offset); }
.odl-chat-thread .odl-sr { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
@container (max-width: 28rem) { .odl-chat-thread .odl-log { padding-inline: var(--ds-space-inset-lg); } .odl-chat-thread .odl-composer { padding-inline: var(--ds-space-inset-sm); } }
@container (max-width: 22rem) { .odl-chat-thread .odl-video { display: none; } }
@media (prefers-reduced-motion: reduce) { .odl-chat-thread, .odl-chat-thread *, .odl-chat-thread *::before { transition: none !important; animation: none !important; } }
</style>
<section class="odl-chat-thread" aria-label="Messages with Ximena Quintana">
<div class="odl-card">
<header class="odl-head">
<button type="button" aria-label="Back to messages"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg></button>
<span class="odl-who"><span class="odl-av" aria-hidden="true"><img src="https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=96&h=96&fit=crop&auto=format&q=70" alt="" width="48" height="48" loading="lazy" decoding="async"><span class="odl-on"></span></span><span><b>Ximena Quintana</b><small>Active now</small></span></span>
<button type="button" aria-label="Voice call"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg></button>
<button type="button" class="odl-video" aria-label="Video call"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5"/><rect x="2" y="6" width="14" height="12" rx="2"/></svg></button>
</header>
<div class="odl-log" role="log" aria-label="Conversation" tabindex="0">
<p class="odl-day">Today 09:41</p>
<div class="odl-row is-in"><span class="odl-slot"></span><div class="odl-stack"><p class="odl-msg is-first">Are you still up for the flea market on Saturday?</p></div></div>
<div class="odl-row is-in"><span class="odl-av is-sm" aria-hidden="true"><img src="https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=64&h=64&fit=crop&auto=format&q=70" alt="" width="32" height="32" loading="lazy" decoding="async"></span><div class="odl-stack"><p class="odl-msg is-last">The ceramics stall opens at 8 and the good bowls go fast</p></div></div>
<div class="odl-row is-out is-gap"><div class="odl-stack"><div class="odl-msg is-first"><p class="odl-quote"><span class="odl-sr">Replying to </span><b>Ximena</b> The ceramics stall opens at 8 and the good bowls go fast</p><p>8 is brutal but fine. I'll bring coffee</p></div></div></div>
<div class="odl-row is-out"><div class="odl-stack"><p class="odl-msg is-last">Meet by the fountain?</p></div></div>
<div class="odl-row is-in is-gap"><span class="odl-av is-sm" aria-hidden="true"><img src="https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=64&h=64&fit=crop&auto=format&q=70" alt="" width="32" height="32" loading="lazy" decoding="async"></span><div class="odl-stack"><p class="odl-msg has-react">Perfect. Oat milk for me please</p>
<span class="odl-react"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></svg><span class="odl-sr">You reacted with a heart</span></span></div></div>
<div class="odl-row is-out is-gap"><div class="odl-stack"><p class="odl-msg">Noted. See you at 7:50</p><p class="odl-seen">Seen 09:52</p></div></div>
<div class="odl-row is-in is-gap odl-is-typing"><span class="odl-av is-sm" aria-hidden="true"><img src="https://images.unsplash.com/photo-1531746020798-e6953c6e8e04?w=64&h=64&fit=crop&auto=format&q=70" alt="" width="32" height="32" loading="lazy" decoding="async"></span><span class="odl-typing" role="status" aria-label="Ximena is typing"><i></i><i></i><i></i></span></div>
</div>
<form class="odl-composer">
<button type="button" aria-label="Attach a photo or file"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg></button>
<label class="odl-sr" for="odl-ct-input">Message</label>
<input id="odl-ct-input" type="text" placeholder="Message…" autocomplete="off" enterkeyhint="send">
<button type="submit" class="odl-send" aria-label="Send" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg></button>
</form>
</div>
</section>
<script>
document.querySelectorAll(".odl-chat-thread").forEach((root) => {
if (root.dataset.odlReady) return; // wire once, even if the file is pasted twice
root.dataset.odlReady = "1";
const form = root.querySelector(".odl-composer"), input = form.querySelector("input"), send = form.querySelector(".odl-send");
const log = root.querySelector(".odl-log"), typing = root.querySelector(".odl-is-typing");
input.addEventListener("input", () => { send.disabled = !input.value.trim(); });
form.addEventListener("submit", (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text) return;
root.querySelectorAll(".odl-seen").forEach((s) => s.remove()); // the receipt sits under the newest message only
const row = document.createElement("div"), stack = document.createElement("div"), msg = document.createElement("p"), seen = document.createElement("p");
row.className = "odl-row is-out is-gap"; stack.className = "odl-stack"; msg.className = "odl-msg"; seen.className = "odl-seen";
msg.textContent = text; seen.textContent = "Sent";
stack.append(msg, seen); row.append(stack);
log.insertBefore(row, typing);
input.value = ""; send.disabled = true; input.focus();
log.scrollTop = log.scrollHeight;
});
log.scrollTop = log.scrollHeight;
});
</script>
A feed post: the author's photo with a follow button, a photo, like, comment, share and save, a like count that changes, a caption that opens with "more" and the top comment.
An album or playlist: a cover photo, title and artist, play, shuffle and save, then numbered tracks with their length, an explicit tag, a like heart and the playing one marked.
Two swipeable rows for a streaming home: Continue watching, with stills, a progress bar and the time left, and Because you watched, with posters and New season labels.