You Might Also Like
Typing Indicator — Chat "..." Dots HTML CSS JS
Typing Indicator · Loaders · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Typing Indicator — Staggered Bouncing Dots in a Chat Bubble

The three bouncing dots that mean "someone is typing…" are a tiny, universally understood piece of UI — and getting the animation and the show/hide logic right is what makes a chat feel alive. This snippet builds the classic typing indicator in plain HTML, CSS, and vanilla JavaScript: a bubble with three dots animated on a staggered loop, plus the show, hide, and auto-hide-on-idle behaviour a real chat needs — no library.
Three dots, one keyframe, staggered
All three dots share a single @keyframes that lifts a dot up and brightens it, then settles. The illusion of a travelling wave comes entirely from animation-delay: the second dot starts 0.18s after the first, the third 0.36s after, so at any moment they're at different points in the same cycle. This delay-stagger technique — one animation, offset start times — is the elegant way to build any sequential dot or bar loader without writing three separate animations. The easing and the dip-to-50%-opacity at rest give it the soft, organic bounce of the real thing.
A proper chat bubble
The dots sit in a bubble styled exactly like an incoming message — same background, same asymmetric border-radius with the squared bottom-left corner that marks it as "from them" — next to the sender's avatar. Matching the indicator to your message bubbles is what makes it read as "this person is about to send a message" rather than a generic spinner; it occupies the same visual slot the real message will.
Show, hide, and auto-hide-on-idle
A typing indicator isn't just an animation — it's state. The snippet includes the logic a real chat uses: showTyping() reveals the bubble and resets a 4-second idle timer that hides it automatically, so a dropped "stopped typing" event never leaves the dots stuck forever. In production you'd call showTyping() each time a typing event arrives over your socket, and hide it when the actual message lands — the timer is the safety net. Exposing showTyping on window makes it easy to wire to your real-time events.
Graceful hide transition
Hiding collapses the row with opacity and height rather than an abrupt display: none, so the indicator fades out and the conversation closes the gap smoothly — the small polish that keeps the chat from jumping when typing stops. The demo button toggles it so you can see both states immediately.
Drop-in for any chat
The markup is just a row you append to your message list, so it drops into any chat UI. Swap the avatar and bubble colours to match your theme, wire showTyping() to your WebSocket or presence channel, and you have the complete typing-indicator behaviour. It's also a clear reference for the delay-stagger animation pattern used across loaders and sequential effects.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Instead of tracing the timing values by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the three dots produce a travelling-wave effect from a single shared keyframe plus staggered animation-delay values, and why showTyping() clears and resets hideTimer on every call rather than letting multiple timers stack up. It's worth asking about optimization too — with many simultaneous chat threads each running their own idle timer, is a single per-conversation setTimeout the right approach, or would a shared scheduler be cheaper? For extending it, have it add a "seen" receipt that appears once typing stops and a message lands, support multiple people typing at once with stacked avatars, or wire showTyping/hideTyping directly to WebSocket presence events. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a chat "typing indicator" (three bouncing dots) in plain HTML, CSS, and vanilla JavaScript with no libraries.
Requirements:
- A message bubble styled identically to an incoming chat message (same background and border-radius, including a squared corner on the side that marks it as "from them"), containing exactly three small dot elements next to a sender avatar.
- All three dots must share a single CSS keyframe animation that lifts each dot up and brightens it before settling back down; do not write three separate animations. Stagger the three dots purely with different animation-delay values (e.g. 0s, 0.18s, 0.36s) so they appear as a travelling wave from one shared keyframe.
- Provide a showTyping() function that reveals the indicator row and starts (or resets, if already running) a several-second idle timer; if showTyping() is not called again before the timer fires, the indicator must hide itself automatically, so a dropped "stopped typing" event from the server never leaves the dots stuck on screen forever.
- Hiding the indicator (whether by timeout or manually) must animate opacity and the row's height down to zero rather than using display: none abruptly, so the surrounding messages close the gap smoothly instead of jumping.
- Expose the show/hide functions in a way that could be called from a WebSocket "typing" event handler in a real application.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA mini chat renders with two messages and a typing indicator (three bouncing dots).
- 2Toggle itClick the button to hide or show the typing bubble and see the fade transition.
- 3Call showTyping() on eventsIn a real app, call showTyping() each time a "typing" event arrives over your socket.
- 4Let it auto-hideIf no further typing event arrives within 4s, the indicator hides itself automatically.
- 5Theme the bubbleMatch the avatar and bubble colours and radius to your chat's message styling.
- 6Drop into your listAppend the row to your message list and remove it when the real message arrives.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
All three dots share the same @keyframes (lift up and brighten, then settle), but each starts at a different time via animation-delay — 0s, 0.18s, 0.36s. Because they're offset within the same cycle, at any instant they're at different phases, producing a travelling wave. This one-animation-plus-staggered-delays technique avoids writing three separate animations and is the standard way to build sequential dot loaders.
Typing indicators are driven by events — "started typing" and "stopped typing" / message-sent. If the stop event is dropped (flaky network, closed tab), the dots can get stuck on forever. A short idle timer that hides the indicator unless refreshed by a new typing event is the safety net every robust chat uses: showTyping() resets the timer each call, so the dots persist only while typing events keep arriving.
Call showTyping() whenever a typing event arrives over your WebSocket or presence channel (it reveals the bubble and resets the idle timer), and hide the row when the actual message arrives or the user goes idle. The snippet exposes showTyping() on window for easy wiring; in a framework you'd call a component method instead. The animation and bubble are purely presentational.
display:none removes the element instantly, which makes the message list jump as the gap closes abruptly. Animating opacity and collapsing the height (with overflow hidden and negative margin) lets the indicator fade out smoothly and the conversation close the space gracefully — the small polish that keeps the chat from feeling jumpy when typing stops.
Render the bubble conditionally on an isTyping state. In React, set isTyping from your socket handler and clear it with a useEffect timeout; in Vue, use a ref with a watcher or timeout; in Angular, a component property with a timer. The dot animation is pure CSS and ports unchanged — only the show/hide state and idle timer move into the framework.