Skeleton Chat Message Loader — Alternating Bubble Placeholders in HTML CSS JS
Skeleton Chat Message Loader · Loaders · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="sc-thread" id="scThread"></div>*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,sans-serif;background:#0b1120;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px}
.sc-thread{width:100%;max-width:400px;background:#111a2e;border:1px solid #223055;border-radius:16px;padding:18px;display:flex;flex-direction:column;gap:14px}
.sc-row{display:flex;align-items:flex-end;gap:8px;max-width:80%}
.sc-row.sc-in{align-self:flex-start}
.sc-row.sc-out{align-self:flex-end;flex-direction:row-reverse}
.sc-avatar{width:26px;height:26px;border-radius:50%;flex-shrink:0;background:linear-gradient(90deg,#1c2846 25%,#2a3a63 37%,#1c2846 63%);background-size:400% 100%;animation:scShimmer 1.6s ease-in-out infinite}
.sc-bubble{height:16px;border-radius:14px;background:linear-gradient(90deg,#1c2846 25%,#2a3a63 37%,#1c2846 63%);background-size:400% 100%;animation:scShimmer 1.6s ease-in-out infinite}
.sc-in .sc-bubble{border-bottom-left-radius:4px;background-color:#16213b}
.sc-out .sc-bubble{border-bottom-right-radius:4px;background-color:#26314f}
@keyframes scShimmer{0%{background-position:200% 0}100%{background-position:-200% 0}}var thread = document.getElementById('scThread');
// Each entry: side (in = received, has avatar / out = sent, no avatar) and a
// bubble width — alternating sides and varied widths is what makes this read
// as chat bubbles rather than a generic skeleton block.
var MESSAGES = [
{ side: 'in', width: 62 },
{ side: 'in', width: 38 },
{ side: 'out', width: 45 },
{ side: 'in', width: 70 },
{ side: 'out', width: 58 },
{ side: 'out', width: 30 },
];
MESSAGES.forEach(function (m, i) {
var row = document.createElement('div');
row.className = 'sc-row sc-' + m.side;
if (m.side === 'in') {
var avatar = document.createElement('div');
avatar.className = 'sc-avatar';
avatar.style.animationDelay = (i * 80) + 'ms';
row.appendChild(avatar);
}
var bubble = document.createElement('div');
bubble.className = 'sc-bubble';
bubble.style.width = m.width + '%';
bubble.style.animationDelay = (i * 80) + 'ms';
row.appendChild(bubble);
thread.appendChild(row);
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Skeleton Chat Message Loader — Alternating Bubbles for a Messaging UI

A generic skeleton list of equal-width bars doesn't read as "a conversation is loading" — it reads as "a list is loading." This snippet is shaped specifically for chat: skeleton placeholders styled as message bubbles, alternating between left-aligned "received" messages (with a small avatar circle) and right-aligned "sent" messages (no avatar), each bubble a different width, so the loading state already looks like a conversation before any real content arrives.
Alternating sides via a data array
Rather than hard-coding rows, a small MESSAGES array holds { side, width } pairs — side is 'in' (received, avatar shown, bubble left-aligned with align-self: flex-start) or 'out' (sent, no avatar, bubble right-aligned via flex-direction: row-reverse and align-self: flex-end). Looping over this array and toggling classes/flex direction per row is what produces the genuine left/right alternation — a structural difference from a skeleton list where every row has identical alignment.
Varied bubble widths read as varied message lengths
Each entry's width (a percentage, 30–70% in the demo) sets that bubble's actual CSS width, so the skeleton bubbles are different sizes the way real chat messages are — a one-word reply next to a longer sentence. A uniform-width skeleton row, by contrast, reads as a table or list rather than conversational text of varying length, which is a big part of what makes this pattern feel chat-specific rather than generic.
Bubble-shaped corners, not bars
Each placeholder uses a large border-radius like a real chat bubble, with one corner flattened (border-bottom-left-radius on received, border-bottom-right-radius on sent) to mimic the little "tail" corner convention most chat UIs use — a detail that immediately reads as "message bubble" rather than "generic skeleton bar," even before the shimmer animation runs.
Avatar only on the received side
Only 'in' rows get a circular avatar placeholder next to the bubble, matching how most chat UIs show the other person's avatar next to their messages but omit your own avatar next to messages you sent — another small alternation that reinforces the specific chat-UI mental model rather than a generic alternating list. Pair it with a chat UI shell or chat message bubbles for the resolved content, or a typing indicator for the moment just before a reply streams in.
Step by step
How to Use
- 1Paste HTML, CSS, and JS6 alternating chat-bubble skeletons render inside a thread container.
- 2Observe the alternationReceived messages sit left with an avatar; sent messages sit right without one.
- 3Observe the widthsEach bubble is a different width, mimicking varied message lengths.
- 4Edit the MESSAGES arrayAdd, remove, or reorder { side, width } entries to change the thread shape.
- 5Replace with real messagesOnce data loads, swap each skeleton row for the actual message bubble.
- 6Restyle the bubblesChange the bubble colors, corner radius, or avatar size to match your chat UI.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
A MESSAGES array holds a side value ('in' or 'out') per row. The row's CSS class and alignment are set from that value: 'in' rows get align-self: flex-start and a leading avatar; 'out' rows get align-self: flex-end and flex-direction: row-reverse with no avatar. Because this comes from real per-row data rather than a fixed CSS pattern like nth-child(even), you can reorder or extend the array freely and the alignment always matches.
Only rows where side is 'in' (received messages) render an avatar element, matching the common chat-UI convention of showing the other person's avatar next to their messages while omitting your own avatar next to messages you sent. This is a conditional render based on the data, not a CSS visibility trick.
Each entry in MESSAGES carries its own width percentage, applied directly as that bubble's CSS width. Real chat messages vary enormously in length, so uniform-width skeleton bars would look like a generic list rather than a conversation. Randomizing or hand-picking varied widths per row is what sells the "chat message" read before any real text loads.
Generate the MESSAGES array from your actual expected message count (or a reasonable guess, like 5-8 rows) rather than hard-coding 6 entries, and randomize width and side if you don't know the real shape yet — Math.random() < 0.5 ? 'in' : 'out' and a random width in a sensible range (30-75%) works well as a stand-in.
Map over your MESSAGES-equivalent array (or a generated placeholder array) in the framework's templating syntax, conditionally rendering the avatar for 'in' rows and binding the width as an inline style or CSS variable on the bubble. The shimmer keyframe and bubble-shape CSS are framework-agnostic and need no changes.