Typing Dots Staggered Bounce
A compact loading indicator with three expression methods: a horizontal translateY row wave, a vertical translateX column wave, and a scale-only pulse that all use staggered animation delays.
A compact loading indicator with three expression methods: a horizontal translateY row wave, a vertical translateX column wave, and a scale-only pulse that all use staggered animation delays.
Loader / stagger / expression methods
Three compact dot loaders share the same status semantics and stagger timing, but each demonstrates a different expression method: row wave, column wave, and scale-only pulse.
Horizontal axis · translateY
A horizontal dot wave: same vertical keyframe, staggered per-dot delay, fixed cross-axis space.
Vertical axis · translateX
A vertical dot wave: stacked dots, top-to-bottom delay, sideways transform inside a reserved column.
No travel · scale + opacity
A no-travel dot pulse: fixed positions, staggered scale and opacity, no vertical or horizontal shift.
Typing dots use one shared dot keyframe and stagger only animation-delay, so a compact status can read as motion without changing the row height.
1<div class="typing-status" role="status" aria-live="polite"><span class="typing-status__label">Assistant is typing</span><span class="typing-dots" aria-hidden="true"><span></span><span></span><span></span></span></div><style>.typing-status {display: inline-flex;align-items: center;gap: .55rem;min-height: 2rem;line-height: 1;color: #dff8ff;}.typing-dots {display: inline-flex;align-items: center;justify-content: center;gap: .28rem;25height: 1rem;}.typing-dots span {width: .42rem;height: .42rem;border-radius: 999px;background: #67e8f9;animation: typing-dot-bounce 1.2s ease-in-out infinite;}.typing-dots span:nth-child(2) {37animation-delay: .16s;}.typing-dots span:nth-child(3) {animation-delay: .32s;}@keyframes typing-dot-bounce {0%, 80%, 100% {46transform: translateY(0);opacity: .45;}40% {transform: translateY(-.34rem);opacity: 1;}}55@media (prefers-reduced-motion: reduce) {.typing-dots span {animation: none;opacity: .72;transform: none;}}</style>
No. Put role="status" and aria-live="polite" on the containing message, then mark the dot group aria-hidden. The accessible text should announce the state once, not every animated dot.
BEFORE animates layout-affecting margin on the dot, so the status row can measure differently as the dots bounce. AFTER keeps a fixed-height row and moves only transform, so the same height: 82px stage stays stable.
Reserve a fixed cross-axis height on the dot wrapper and keep the status row line-height stable. The animated dots can travel inside that reserved space without changing the surrounding layout.
Both examples use the same keyframe. BEFORE starts every dot together, so the loader blinks in sync. AFTER staggers animation-delay and reads as a staggered wave.
A shared keyframe keeps the bounce shape consistent. Staggering animation-delay changes phase only, which is easier to tune and makes the three-dot wave reusable across chat, inline, and agent-status surfaces.
Transform moves the painted dot without changing layout. top, margin, or height can alter line metrics, push neighboring text, or cause below content to shift while the loader loops.
Disable the bounce and show the three dots at a steady opacity. Do not replace it with a fade, wave, or alternate loop; the reduced-motion state should be static.
Typing dots are a compact indefinite loader: three small circles use staggered animation-delay so they read as a wave instead of blinking in sync. The pattern is not inherently chat-specific, and the showcase should vary the expression method: a horizontal translateY() row wave, a vertical translateX() column wave, or a no-travel scale pulse.
Use this when the system is actively working but cannot report determinate progress. It is strongest for short waits in tight UI: composing a reply, checking a field, or waiting on an assistant step. Avoid it for long-running work where users need progress, retry affordances, or a cancel path.
Build a semantic status wrapper with role="status" and aria-live="polite", then mark the dot group aria-hidden. The dots use identical size and shape inside each expression method; :nth-child() offsets the second and third dot with staggered animation-delay. Reserve the wrapper footprint for the chosen method so row bounce, column nudge, or scale pulse all happen inside stable line metrics.
Do not animate top, margin, orheight for the bounce. Those properties can re-measure inline content, push neighboring controls, or make UI below the row jump on every loop. Keep the moving property to transform, and keep the cycle calm enough that the loader reads as waiting rather than urgency.
The dots are visual decoration; the status copy carries the accessible meaning. Under prefers-reduced-motion: reduce, stop the bounce and show the dots at a steady opacity. Do not replace the bounce with another fade or pulse, and do not leave a typing indicator running after the request has failed or completed.
Typing dots are a loader timing pattern, not a chat-only component. The reusable mechanism is staggered animation-delay over fixed-position dots; the expression can be a horizontal row wave, a vertical column wave, or a no-travel scale pulse.
The production risk is layout movement and misleading status. Reserve the wrapper footprint for the chosen expression, animate transform rather than margin or height, expose a single polite status message, and stop the indefinite loop when the real state resolves or fails.