https://liveblocks.io/blog/how-to-animate-multiplayer-cursors
* Features
* Developers
* Examples
* Company
* Pricing
Sign inSign up
Guides
How to animate multiplayer cursors
Smoothly rendering live cursors is more difficult than it sounds when
real-world conditions are taken into account--here's a quick
introduction to a few different methods, plus some React snippets.
July 5th, 2022
[]Picture of Chris NicholasPicture of Chris Nicholas
Chris Nicholas
@ctnicholasdev
How to animate multiplayer cursorsHow to animate multiplayer cursors
*
*
*
Multiplayer cursors are becoming an increasingly common sight across
all sorts of collaborative tools, but have you ever wondered how
they're animated? Animating real-time cursors is more complex than it
first may seem, thanks to network and connection limitations. Here's
a quick overview of a few different methods, with some React snippets
to get you started. Let's dive in!
Why do we need animations?
In an ideal world, animations wouldn't be needed, and every single
cursor movement would be reflected immediately across browsers.
An interactive demo displaying a perfectly animated cursor.
You
Others
However, updates can't be transmitted instantly, and realistically,
we wouldn't want them to be. Even if updates were sent every
millisecond (1ms), blinking your eye still takes longer than
100ms--would it really be efficient to send 100 updates in such a
short space of time?
Throttling
Every real-time service will use some kind of throttling to ensure
that your code, and their systems, aren't overloaded, and this is
where our animation problem lies. Try adjusting the throttle rate
with the range slider below.
An interactive demo allowing you to change the update frequency of
cursors.
You
Others
Update every 120ms
[120 ]
Lowering the throttle rate prevents updates being sent as regularly,
and as you can see, this results in the cursor animation only being
completely smooth at the lowest rates.
In the real world
Of course in the real world, the cursor won't update exactly on the
throttle rate (for example, 120ms) there'll be some slight variation
between each update caused by varying processing times on clients and
server, as well as changing network latency. The actual time between
updates will be slightly different.
An diagram explaining that update time isn't just throttling.
Throttle rate (120ms)
Variation (5-15ms)
I'm going to call the actual time between updates the update rate, a
value that includes throttling, and other normal variations. If you'd
like to see a realistic update rate, try interacting with the demo
below--it connects to Liveblocks and manually measures the rate on
each update.
A demo that displays your current update frequency and ping for
Liveblocks when interacted with.
You
Move cursorTouch and drag
here to calculate
Update rate
0ms
Ping
0ms
This demo uses Liveblocks' default throttle rate, but you can specify
a custom rate within createClient.
Animation methods
Now we've seen the problem, let's fix it. There are three different
ways we can tackle this: CSS transitions, spring animations, and
spline animations.
In the React code examples below, we'll be animating cursors by
passing x and y props to the component, these numbers corresponding
to the pixel distance from the top-left corner of the container.
CSS Transitions
The easiest approach to animate cursor locations is to use a CSS
transition on the cursor component which can be added with just a
single CSS property, transition.
An interactive demo displaying a cursor animated with CSS
transitions.
You
Others
Update every 120ms
[120 ]
When you increase the update time, you'll probably notice the most
prominent flaw with CSS transitions--the route to the next point is
always a straight line. CSS transitions only consider the cursor's
next coordinates, not the path taken to get there, nor the momentum
of the cursor.
If you move the slider in the diagram below, you can see the shape of
the original cursor's movement alongside the path created by the
transition. Each "x" represents a new update that's been received.
An interactive diagram showing the path taken by a CSS transition
animated cursor, alongside the original cursor's path.
Move slider
Cursor movement
[0 ]
[*]
Show updates
[*]
Show paths
As you can see, each time an update is received, the transition
starts pathing a straight line directly towards the next update;
certainly less than ideal.
Timing functions
When it comes to CSS timing functions, interestingly, and perhaps
counterintuitively, linear transitions result in smoother cursors
than easing transitions. Try enabling ease-in-out, then going back to
linear:
An interactive demo displaying a cursor animated with CSS
transitions.
You
Others
Update every 120ms
[120 ]
[ ]
Ease-in-out
But how does that make sense? Transitions such as ease-in-out slow
down towards the start and end of each transition, but with cursors
we don't want that--we want a similar speed maintained between
updates. If the cursor slows down on each update, before speeding up
again, it won't look as smooth. Using linear ensures that a similar
speed is maintained, and the shift to the next set of coordinates is
smoother.
Should I use CSS transitions?
I'd only recommend using CSS transitions if you're after a
lightweight solution that doesn't use any third-party packages.
Here's an example of a React component that uses CSS transitions:
Cursor.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// CSS transition animated cursor
export default function Cursor({ color, x, y }) {
return (
);
}
// SVG cursor shape
function CursorSvg({ color }) {
return (
);
}
Springs
Rather than using straight-line CSS transitions, we can look to
spring physics to mimic more organic motion. Spring animations allow
us to control aspects of cursor movement such as stiffness and
damping ratio, lending the appearance of a real item moving with its
own impetus and mass, resulting in a much more natural movement.
An interactive demo displaying a cursor animated with springs.
You
Others
Update every 120ms
[120 ]
Spring animations tend to be far smoother than CSS transitions, as
they take into account not just the next coordinates, but also the
current momentum of the element.
An interactive diagram showing the path taken by a spring animated
cursor, alongside the original cursor's path.
Move slider
Cursor movement
[0 ]
[*]
Show updates
[*]
Show paths
After each update, the cursor's direction is smoothly changed before
it paths a straight-line to the next coordinates.
Should I use spring animations?
If you're after smooth cursors with a quick response, and strict
pathing accuracy isn't important, spring animations are the way to
go. Here's an example of a spring-animated React cursor built with
Framer Motion.
Cursor.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { motion } from "framer-motion";
// Spring animated cursor
export default function Cursor({ color, x, y }) {
return (
);
}
// SVG cursor shape
function CursorSvg({ color }) {
return (
);
}
Spring animations in use
A good example of an app using spring-animated cursors is our
open-source project Pixel Art Together, which uses Svelte's built-in
spring library.
Video displaying live cursors and collaboration in Pixel Art
Together.
Splines
Spline interpolation is a method of constructing new points from a
set of known points, and plotting a smooth curve between. It's often
used to plot curves in charts between discrete points of data.
An graph with points connected by a spline curve.
We can make use of spline interpolation to animate smooth paths for
cursors, relying on it using multiple different points to create a
more accurate path. There is a downside to this accuracy however--the
function waits to receive multiple points before rendering, so the
cursor is slightly delayed.
An interactive demo displaying a cursor animated with splines.
You
Others
Update every 120ms
[120 ]
Spline animations take into account both the previous coordinates,
and the next coordinates, to create an accurate path that passes
directly through every point, unlike the other animation types.
An interactive diagram showing the path taken by a spline animated
cursor, alongside the original cursor's path.
Move slider
Cursor movement
[0 ]
[*]
Show updates
[*]
Show paths
As you can tell, the path created isn't 100% accurate--splines still
struggle with abrupt changes of direction--but it is much closer than
any other method.
Should I use spline animations?
I'd recommend using spline-animated cursors where accuracy is
preferred and a little delay is acceptable. The easiest way to do
this in React is with the perfect-cursors library, built by the
creator of tldraw.
Cursor.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import {
useCallback,
useEffect,
useRef,
useLayoutEffect,
useState,
} from "react";
import { PerfectCursor } from "perfect-cursors";
// Spline animated cursor
export default function Cursor({ color, x, y }) {
const rCursor = useRef(null);
const animateCursor = useCallback((point) => {
const elm = rCursor.current;
if (!elm) return;
elm.style.setProperty(
"transform",
`translate(${point[0]}px, ${point[1]}px)`
);
}, []);
const onPointMove = usePerfectCursor(animateCursor);
useEffect(() => onPointMove([x, y]), [onPointMove, x, y]);
return (
);
}
// Hook for adding perfect-cursors
function usePerfectCursor(cb, point) {
const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
const [pc] = useState(() => new PerfectCursor(cb));
useIsomorphicLayoutEffect(() => {
if (point) {
pc.addPoint(point);
}
return () => pc.dispose();
}, [pc]);
return useCallback((point) => pc.addPoint(point), [pc]);
}
// SVG cursor shape
function CursorSvg({ color }) {
return (
);
}
Spline animations in use
A great example of spline cursors in the wild is tldraw, which uses
perfect-cursors.
Video displaying live cursors and collaboration in tldraw.
Comparison
Now that we've taken a look the different animation methods, let's
see how they perform side-by-side.
An interactive demo displaying all animated cursors at once.
You
Others
Update every 120ms
[120 ]
When we compare we can really start to notice the delay caused by
spline animations, and also, somewhat surprisingly, we can see just
how similar CSS transitions and spring animations are, despite
springs feeling much more fluid.
An interactive diagram showing the paths taken by all different
animated cursors, alongside the original cursor's path.
Move slider
Cursor movement
[0 ]
[*]
Show updates
[ ]
Show paths
CSS transitions make for a quick-and-easy lightweight solution,
whereas spring and spline animations result in a smoother experience.
Springs work smoothly, but if you're after accuracy, and
responsiveness isn't important, take a look into splines!
Looking to add real-time cursors?
This article covers the front end of live cursors, but what about the
back end? Implementing this is a much more difficult task, but it
doesn't have to be--you can let Liveblocks handle your collaborative
back end for you. We'll do all the heavy lifting, so you can work on
building your app.
Cursor positions, along with any other multiplayer data, can be sent
across clients with updatePresence(), part of the @liveblocks/client
package.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Send cursor coordinates on movement
function onPointerMove(event) {
room.updatePresence({
cursor: {
x: event.clientX,
y: event.clientY,
},
});
}
// Remove cursor position when not on page
function onPointerLeave() {
room.updatePresence({
cursor: null,
});
}
Changes to other users' presence can then be detected on clients
using subscribe("others").
1
2
3
4
5
// Callback runs on presence updates
room.subscribe("others", (others) => {
// Array of objects containing user data
others.toArray();
});
We also have @liveblocks/react, a special React package that
simplifies rendering multiplayer components even further. You can add
useOthers() which replaces the subscription above:
1
2
3
4
5
6
7
8
9
10
11
function LiveCursors() {
const others = useOthers();
return (
);
}
Find your framework
We have working examples of live cursors built in a number of
different frameworks to get you started, have a try: Next.js, Vue,
Svelte, Solid, JavaScript.
There's more to life than cursors
Wait, really? This article is about live cursors, but these tips also
apply to other animated components in multiplayer environments, for
example sticky notes on a collaborative whiteboard. We have plenty of
other fun examples too!
Ready to get started?
Join developers who use Liveblocks to build world-class real-time
multiplayer experiences.
Sign up for free
Product
* Presence
* Storage
* Analytics
* Pricing
Developers
* Documentation
* API reference
* System status
* Release notes
Company
* Blog
* About us
* Careers
* Customers
Community
* GitHub
* Discord
* Twitter
* YouTube
Resources
* Examples
* Contact us
* Terms of service
* Privacy policy
* Press kit
(c) 2022 Liveblocks Inc.