4.2 KB gzipped 128 classes 0 JS transform + opacity only

Motion that makes a chat feel alive.

One tiny stylesheet of GPU-composited animations for LLM interfaces — message entrances, streaming text, typing dots, button physics, staggered lists. Drop a class on an element and ship it.

live · fx-msg / fx-dots / fx-stream / fx-cursor

Entrances

21 · tap ↻ (or the card) to replay · tap the class to copy

Exits

12 · pairs with entrances for dismiss / delete / collapse

Attention

15 · errors, confirmations, nudges · add .fx-loop to repeat

Buttons & hover

11 · pure transitions — on touch screens these fire on press

Lists & stagger

6 variants · children cascade automatically, no per-item classes
  • Search the web tool
  • Summarize this thread suggestion
  • Generate an image tool
  • Draft a reply suggestion
  • Explain the error suggestion
Speed & delay modifiers
  • fx-up fx-faster .2s
  • fx-up .45s default
  • fx-up fx-slow .7s
  • fx-up fx-spring overshoot ease
  • fx-up fx-d3 +.3s delay

Rotating updates

11 · cycling status text, the way thoughts surface and dissolve while a model works
Thinking… Searching the web… Reading 12 sources… Synthesizing the answer…
.fx-cycle-4 — pure CSS, zero JS

In-flight

13 · something is being worked on — a line draws, a streak travels, a fill sweeps

Width & size

10 · growing and collapsing panels, pills, inputs, sidebars

Chat & loading primitives

11 · purpose-built for LLM interfaces

Usage

Include the stylesheet once. Add a class when an element should move. Tune everything from :root.

<!-- entrances run when the element (or class) is added to the DOM -->
<div class="fx-msg">Assistant reply</div>
<div class="fx-msg-user">User message</div>
<span class="fx-stream">each streamed chunk</span>
<ul class="fx-stagger fx-stagger-left"></ul>
<button class="fx-btn fx-lift fx-ring">Send</button>
<div class="fx-sheet-up">Bottom sheet</div>

<!-- rotating status — zero JS, each child takes a turn -->
<div class="fx-cycle-3" style="--fx-cycle:5s">
  <span>Searching…</span><span>Reading…</span><span>Writing…</span>
</div>

<!-- JS-driven swap: old thought out, new thought in -->
old.classList.add('fx-thought-out');
fresh.classList.add('fx-thought');  // then remove `old` on animationend

<!-- width morphing: toggle any width and it tweens -->
<aside class="fx-width" style="width:64px"></aside>  // set width:240px to expand

/* global tuning — make the whole app snappier or softer */
:root { --fx-dur: .35s; --fx-dist: 10px; --fx-stagger: 45ms; }

/* exits: add the class, remove the node on `animationend` */
el.classList.add('fx-up-out');
el.addEventListener('animationend', () => el.remove(), { once: true });
Copied