AI CSS Animations

Streaming Response Loader

Word-shaped tokens stream in from blur, arriving in bursts like a model writing, and the newest ones flash brighter before settling. Driven by one registered @property.

Also known as: ai streaming text animation, chatgpt response loading, token streaming effect.

HTML

<div class="ldr-streaming-response" role="status" aria-label="Generating response">
  <div class="ldr-streaming-response-line"><span style="--i: 0; --w: 4"></span><span style="--i: 1; --w: 6"></span><span style="--i: 2; --w: 3"></span><span style="--i: 3; --w: 5"></span></div>
  <div class="ldr-streaming-response-line"><span style="--i: 4; --w: 6"></span><span style="--i: 5; --w: 3"></span><span style="--i: 6; --w: 4"></span><span style="--i: 7; --w: 6"></span></div>
  <div class="ldr-streaming-response-line"><span style="--i: 8; --w: 5"></span><span style="--i: 9; --w: 4"></span></div>
</div>

CSS

@property --ldr-streaming-response-t {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
.ldr-streaming-response {
  --s: var(--ldr-size, 48px);
  --c: var(--ldr-color, #3b82f6);
  display: grid;
  gap: calc(var(--s) / 8);
  animation: ldr-streaming-response-stream 3.6s linear infinite;
}
.ldr-streaming-response-line {
  display: flex;
  gap: calc(var(--s) / 10);
}
.ldr-streaming-response-line span {
  /* r: 0 to 1 as this token arrives; fresh: 1 right after it lands, fading to 0 */
  --r: clamp(0, calc(var(--ldr-streaming-response-t) * 11 - var(--i)), 1);
  --fresh: calc(1 - clamp(0, calc((var(--ldr-streaming-response-t) * 11 - var(--i) - 1) / 2), 1));
  width: calc(var(--s) * var(--w) / 6);
  height: calc(var(--s) / 7);
  border-radius: calc(var(--s) / 14);
  background: color-mix(in srgb, var(--c) calc(40% + var(--fresh) * 60%), transparent);
  opacity: var(--r);
  transform: translateY(calc((1 - var(--r)) * var(--s) / 10));
  filter: blur(calc((1 - var(--r)) * var(--s) / 14));
}
@keyframes ldr-streaming-response-stream {
  0%   { --ldr-streaming-response-t: 0; opacity: 1; }
  18%  { --ldr-streaming-response-t: 0.32; }
  28%  { --ldr-streaming-response-t: 0.36; }
  52%  { --ldr-streaming-response-t: 0.8; }
  60%  { --ldr-streaming-response-t: 0.84; }
  72%  { --ldr-streaming-response-t: 1; }
  88%  { opacity: 1; }
  100% { --ldr-streaming-response-t: 1; opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .ldr-streaming-response, .ldr-streaming-response *, .ldr-streaming-response::before, .ldr-streaming-response::after, .ldr-streaming-response *::before, .ldr-streaming-response *::after {
    animation-duration: 3s;
  }
}

Customizing this loader

  • Color: set --ldr-color on the element or a parent. Currently previewing #3b82f6.
  • Size: set --ldr-size. Every dimension in the snippet is derived from it. Currently previewing 64px.
  • Speed: change the duration on the animation shorthand. Delays on the children are relative, so they keep their rhythm.
  • Accessibility: the root carries role="status" and an aria-label so screen readers announce the wait. The reduced-motion rule at the end slows the animation for users who asked for less motion.

Similar loaders