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 copyExits
12 · pairs with entrances for dismiss / delete / collapseAttention
15 · errors, confirmations, nudges · add .fx-loop to repeatLists & 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 sweepsWidth & size
10 · growing and collapsing panels, pills, inputs, sidebarsChat & loading primitives
11 · purpose-built for LLM interfacesUsage
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 });