Product updates

The cursors and the reactions left the app — and became two open-source packages

Live cursors and flying reactions moved out of the board into live-cursors and live-reactions: two MIT packages on npm that work over any channel, with the mechanics nobody ships — throttling, TTL, rate limits, clustering and an untrusted-wire protocol.
August 2, 2026Open sourcePlatform
Release details
Structured release note page with localized markdown content.
Last updated: August 2, 2026

What we did

Two features of a live board — the cursors of everyone else moving over the cards, and the emoji that fly up when the team reacts — used to be a few hundred lines wired straight into our Vue components. They worked. They were also the only part of the product with no way to test them: multiplayer behaviour needs two clients, and two clients existed only in production.

Both are now separate packages, released under MIT and published to npm:

QRetro installs them the same way you would. There is no internal fork and no "showcase" copy of the code: the board runs exactly what is on npm.

Why any of this needed writing

Cursors look like a weekend feature until you build them. The existing options all ask you to adopt their world — Liveblocks, Velt and Ably are hosted services with their own bill; y-presence wants a CRDT; cursor-party wants a platform; nuxt-live-cursors wants Nitro WebSockets. The one library that stays out of the way, perfect-cursors, interpolates positions and nothing else, and has not shipped since 2022. Reactions are worse: the confetti engines draw particles beautifully and know nothing about who sent one or how often; the complete implementations are locked inside applications like Jitsi and Nextcloud Talk.

The interesting part was never the animation. It is everything around it, and that is what these packages are.

The parts that are actually hard

Coordinates that mean the same thing to everyone. Normalising a pointer against the viewport puts a cursor at the same screen position for everyone — right for a presentation, wrong for a retro board, where participants scroll independently and half the team is looking at a different column. Cursors are normalised against the scrollable content of a chosen element, so a cursor sits on the card it is pointing at, not on the pixel it happened to occupy on someone else's laptop. Both spaces ship, and you can write your own for a zoomable canvas.

A throttle that never drops the last move. Updates go out at ~25 per second, leading and trailing. A plain leading throttle looks fine while the mouse is moving and then freezes the cursor mid-screen the moment it stops — the final position is the one that gets swallowed. That single trailing call is the difference between a live board and a haunted one.

Clients that vanish without saying goodbye. A closed lid, a dead socket, a browser that never fires beforeunload: nothing tells you they left. Every cursor carries a TTL and disappears after three seconds of silence, while presence events remove people instantly when the channel does know. Belt and braces, because either one alone leaves ghosts on the board.

Nothing upstream is protecting you. Both features ride on client events, which by design go around the application server — so no backend rate limit stands between a held-down reaction button and everyone else's screen. The packages carry their own: a continuously refilling token bucket on sending (five in a burst, two per second), a release queue with a stagger between particles, a cap on how many can be in flight, and a hard cap on how many remote participants can be tracked at all. A flooding client can waste its own frames and no one else's.

Every message is treated as hostile. The wire format is versioned; coordinates are clamped into range rather than trusted; an emoji is a string with a length limit, so nobody pushes kilobytes at the room. When the channel itself knows who sent a message — an Echo presence channel does — that identity wins over the one inside the payload, so a client cannot broadcast as somebody else. Labels and emoji are inserted as text nodes and the participant tint travels through a CSS custom property, so content arriving from another browser is never parsed as markup.

Reactions behave like a room, not like a counter. Ten people pressing 👍 should not produce ten separate emoji. Identical reactions from different people gather into one balloon that wiggles, grows with every addition, and scatters when it gets big enough — while one person tapping the same emoji repeatedly just gets their own reactions, one after another. That asymmetry is the whole trick: it rewards a room agreeing and ignores a single enthusiast.

Server rendering, honestly. Transports are passed as factories, so nothing touches WebSocket while a page renders on the server — with a test that fails if it ever does again.

What that bought us

Multiplayer that was previously verifiable only by opening two laptops now runs in CI: unit tests in a simulated DOM, plus Playwright suites that drive two browser contexts through a fifty-line relay and assert that a cursor moved on the other screen. The demo GIFs in both READMEs are recorded by a script that scripts pointer paths and films a third participant, so they can never drift from what the code does.

Each package also ships an llms.txt — a compact machine-readable reference of every export, so a coding assistant integrating one of them reads the API instead of guessing it.

Why it matters for QRetro

The privacy claim on a live board is now something you can check rather than something we assert. A cursor message is a participant id and two numbers; a reaction is an id and an emoji. No names, no avatars, no positions decided by the sender — the receiving client picks where things fly from. Nothing is persisted anywhere: the library holds cursors in memory and forgets them. That paragraph used to be a promise in a policy, and it is now a file you can open.

The rest is unglamorous and real. Code that lives in the open gets read, typed, documented and tested to a standard that internal glue never reaches, and the board inherits all of it — including the bugs found by people using these packages in applications that have nothing to do with retrospectives.

If you build anything collaborative, take them. npm i live-cursors live-reactions, bring your own channel.

More releases

View all releases