20 CSS Loaders 16 / 20
CSS Typing Dots Loader
Four conversational CSS loaders — a chat-bubble typing indicator, an inline AI-thinking status, a cursor blink, and a bouncing dot trio — matching the patterns used in messaging and AI product UIs.
This is a full-page demo — interact inside the frame above, or open it in the playground for the full-screen experience.
The code
<div class="ld-16">
<div class="ld-16__stage">
<div>
<div class="ld-16__label-row">Chat bubble</div>
<div class="ld-16__bubble"><span></span><span></span><span></span></div>
</div> <div class="ld-16">
<div class="ld-16__stage">
<div>
<div class="ld-16__label-row">Chat bubble</div>
<div class="ld-16__bubble"><span></span><span></span><span></span></div>
</div>.ld-16,.ld-16 *,.ld-16 *::before,.ld-16 *::after{box-sizing:border-box;margin:0;padding:0}
.ld-16{
--bg:#1c1c2e;--c1:#e2e8f0;--c2:#94a3b8;--c3:#38bdf8;--c4:#a78bfa;
background:var(--bg);display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:'Segoe UI',sans-serif;
}
.ld-16__stage{display:flex;gap:28px;flex-direction:column;align-items:flex-start;padding:40px;max-width:400px;width:100%}
.ld-16__label-row{color:rgba(255,255,255,.3);font-size:10px;letter-spacing:1.5px;text-transform:uppercase;margin-bottom:4px}
/* Chat bubble with typing dots */
.ld-16__bubble{background:#2d2d44;border-radius:18px 18px 18px 4px;padding:14px 18px;display:inline-flex;align-items:center;gap:6px;max-width:180px}
.ld-16__bubble span{width:8px;height:8px;border-radius:50%;background:var(--c2);animation:ld-16-typing 1.2s ease-in-out infinite}
.ld-16__bubble span:nth-child(1){animation-delay:0s}
.ld-16__bubble span:nth-child(2){animation-delay:.2s}
.ld-16__bubble span:nth-child(3){animation-delay:.4s}
@keyframes ld-16-typing{0%,60%,100%{transform:translateY(0);opacity:.4}30%{transform:translateY(-6px);opacity:1}}
/* Inline text dots */
.ld-16__inline{display:flex;align-items:center;gap:8px;color:var(--c2);font-size:14px}
.ld-16__inline-dots{display:flex;gap:3px}
.ld-16__inline-dots span{width:5px;height:5px;border-radius:50%;background:var(--c3);animation:ld-16-typing 1s ease-in-out infinite}
.ld-16__inline-dots span:nth-child(1){animation-delay:0s}
.ld-16__inline-dots span:nth-child(2){animation-delay:.15s}
.ld-16__inline-dots span:nth-child(3){animation-delay:.3s}
/* Cursor blink */
.ld-16__cursor-wrap{color:var(--c1);font-size:16px;display:flex;align-items:center;gap:2px}
.ld-16__cursor{width:2px;height:20px;background:var(--c4);animation:ld-16-blink 1s step-end infinite}
@keyframes ld-16-blink{0%,100%{opacity:1}50%{opacity:0}}
/* Processing dots large */
.ld-16__processing{display:flex;gap:10px;align-items:center}
.ld-16__processing span{width:12px;height:12px;border-radius:50%;background:var(--c4);animation:ld-16-proc 1.4s ease-in-out infinite}
.ld-16__processing span:nth-child(1){animation-delay:0s}
.ld-16__processing span:nth-child(2){animation-delay:.2s;background:var(--c3)}
.ld-16__processing span:nth-child(3){animation-delay:.4s;background:var(--c4)}
@keyframes ld-16-proc{0%,80%,100%{transform:scale(.6) translateY(0);opacity:.4}40%{transform:scale(1) translateY(-10px);opacity:1}}
@media(prefers-reduced-motion:reduce){
.ld-16__bubble span,.ld-16__inline-dots span,.ld-16__cursor,.ld-16__processing span{animation:none}
} .ld-16,.ld-16 *,.ld-16 *::before,.ld-16 *::after{box-sizing:border-box;margin:0;padding:0}
.ld-16{
--bg:#1c1c2e;--c1:#e2e8f0;--c2:#94a3b8;--c3:#38bdf8;--c4:#a78bfa;
background:var(--bg);display:flex;align-items:center;justify-content:center;min-height:100vh;font-family:'Segoe UI',sans-serif;
}
.ld-16__stage{display:flex;gap:28px;flex-direction:column;align-items:flex-start;padding:40px;max-width:400px;width:100%}
.ld-16__label-row{color:rgba(255,255,255,.3);font-size:10px;letter-spacing:1.5px;text-transform:uppercase;margin-bottom:4px}
/* Chat bubble with typing dots */
.ld-16__bubble{background:#2d2d44;border-radius:18px 18px 18px 4px;padding:14px 18px;display:inline-flex;align-items:center;gap:6px;max-width:180px}
.ld-16__bubble span{width:8px;height:8px;border-radius:50%;background:var(--c2);animation:ld-16-typing 1.2s ease-in-out infinite}
.ld-16__bubble span:nth-child(1){animation-delay:0s}
.ld-16__bubble span:nth-child(2){animation-delay:.2s}
.ld-16__bubble span:nth-child(3){animation-delay:.4s}
@keyframes ld-16-typing{0%,60%,100%{transform:translateY(0);opacity:.4}30%{transform:translateY(-6px);opacity:1}}
/* Inline text dots */
.ld-16__inline{display:flex;align-items:center;gap:8px;color:var(--c2);font-size:14px}
.ld-16__inline-dots{display:flex;gap:3px}
.ld-16__inline-dots span{width:5px;height:5px;border-radius:50%;background:var(--c3);animation:ld-16-typing 1s ease-in-out infinite}
.ld-16__inline-dots span:nth-child(1){animation-delay:0s}
.ld-16__inline-dots span:nth-child(2){animation-delay:.15s}
.ld-16__inline-dots span:nth-child(3){animation-delay:.3s}
/* Cursor blink */
.ld-16__cursor-wrap{color:var(--c1);font-size:16px;display:flex;align-items:center;gap:2px}
.ld-16__cursor{width:2px;height:20px;background:var(--c4);animation:ld-16-blink 1s step-end infinite}
@keyframes ld-16-blink{0%,100%{opacity:1}50%{opacity:0}}
/* Processing dots large */
.ld-16__processing{display:flex;gap:10px;align-items:center}
.ld-16__processing span{width:12px;height:12px;border-radius:50%;background:var(--c4);animation:ld-16-proc 1.4s ease-in-out infinite}
.ld-16__processing span:nth-child(1){animation-delay:0s}
.ld-16__processing span:nth-child(2){animation-delay:.2s;background:var(--c3)}
.ld-16__processing span:nth-child(3){animation-delay:.4s;background:var(--c4)}
@keyframes ld-16-proc{0%,80%,100%{transform:scale(.6) translateY(0);opacity:.4}40%{transform:scale(1) translateY(-10px);opacity:1}}
@media(prefers-reduced-motion:reduce){
.ld-16__bubble span,.ld-16__inline-dots span,.ld-16__cursor,.ld-16__processing span{animation:none}
}How this works
The chat bubble typing indicator wraps three dots inside a styled div with border-radius: 18px 18px 18px 4px to create the asymmetric chat-bubble tail. Each dot uses a single keyframe: translateY(0) → translateY(-6px) → translateY(0) with ease-in-out, cascaded with 0.2s delays per dot. The dots also fade between opacity:0.4 and opacity:1 in the same keyframe so they dim as they rise, reinforcing depth.
The cursor blink uses animation-timing-function: step-end so the cursor snaps between visible and invisible rather than fading — this matches the hard-cut behaviour of real text cursors. The processing dots use cubic-bezier easing on a compound transform of scale and translateY in the same keyframe, making each dot simultaneously grow and rise at its peak, then shrink as it falls back.
Customize
- Match your chat app's bubble shape by adjusting the four
border-radiusvalues on.ld-16__bubble— use18px 18px 4px 18pxfor an outgoing-message variant. - Slow the typing animation to
1.8sfor a more contemplative AI-thinking feel, or drop to0.8sfor an eager autocomplete indicator. - Change dot size from
8pxto5pxfor a compact inline variant that fits within a single line of body text. - Add a fourth dot to the bounce sequence by adding a
span:nth-child(4)rule withanimation-delay: .6sfor a slightly longer visual rhythm. - Swap
translateY(-6px)forscale(1.4)to create a radial pulse variant instead of an upward bounce — useful for non-directional contexts.
Watch out for
- The cursor blink must use
step-endnotsteps(1, end)— while semantically equivalent, some linters flag the latter; use the keyword form for clarity. - The bubble border-radius asymmetry only creates a bottom-left tail pointing left — for a right-aligned outgoing bubble, swap the last two radius values and mirror with
transform: scaleX(-1). - Staggered
animation-delayon dots causes the first animation cycle to start at different offsets — addanimation-fill-mode: bothto each dot to prevent a flash on the initial render.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 49+ | 9+ | 44+ | 49+ |
Typing indicator loaders use universal CSS; step-end timing function is supported in all modern browsers.