
Table of contents
Displaying other users' cursors live on-screen has always been tricky to implement... but no longer! Using Liveblocks on Next.js & Vercel, we can get it working in a matter of minutes. Open this page in another window, side-by-side, to see it in action!
What is Liveblocks?
Liveblocks is a set of APIs and tools (released just two days ago!) built to assist with creating collaborative experiences, such as shared drawing tools, shared forms, and seeing live cursors. I've created live cursors before, with a custom websocket server, and I cannot emphasise how much easier this is!To check out the working demo on this page, open this article in two browser windows, side-by-side, and try moving your cursor around. You should see something like the demo above!
Sign up & Install
First, we need to sign up to Liveblocks (side note: their website design is magical). Their free plan allows for 5 connections in one room, and up to 30 connections across all rooms.
After verifying your email, hop into the Dashboard and make note of your API key (it'll look similar to this):
sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
From this point you can follow along on CodeSandbox, if you don't have a project ready:
Install packages
Next we need to install the Liveblocks packages to our Next.js project:
npm install @liveblocks/client @liveblocks/react @liveblocks/node
And set the Next.js LIVEBLOCKS_SECRET_KEY
environment variable to the secret key:
LIVEBLOCKS_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Authenticating
To connect to Liveblocks we need to setup a simple Vercel API endpoint. This will allow us to connect without exposing our secret key.
Create a new file within /pages/api/
and name it auth.js
:
import { authorize } from '@liveblocks/node'
const secret = process.env.LIVEBLOCKS_SECRET_KEY
// Connect to Liveblocks
export default async function auth (req, res) {
const room = req.body.room
const result = await authorize({ room, secret })
return res.status(result.status).end(result.body)
}
Connect to the endpoint
To connect the front-end, we need to create a new Liveblocks client and pass it to the LiveblocksProvider
component. If your whole
website is using Liveblocks it's advised to place the component within _app.js
, and wrap your entire app in it:
import { createClient } from '@liveblocks/client'
import { LiveblocksProvider } from '@liveblocks/react'
// Create a Liveblocks client
const client = createClient({
authEndpoint: "/api/auth"
})
// Wrap app within LiveblocksProvider
function MyApp({ Component, pageProps }) {
return (
<LiveblocksProvider client={client}>
<Component {...pageProps} />
</LiveblocksProvider>
)
}
export default MyApp
Cursor component
Now we can start developing our app. First up, we'll create a quick cursor component:
We'll be moving the cursor around using transform: translate()
, along with a transition
property to smooth
the animation between every Liveblocks update.
// Simple arrow shape
const CursorImage = () => <svg><path fill="currentColor" d="M8.482,0l8.482,20.36L8.322,17.412,0,20.36Z" transform="translate(11 22.57) rotate(-48)" /></svg>
// Give cursor absolute x/y positioning
export default function Cursor ({ x = 0, y = 0 }) {
return (
<div style={{
color: 'black',
position: 'absolute',
transform: `translate(${x}px, ${y}px)`,
transition: 'transform 0.5s cubic-bezier(.17, .93, .38, 1)'
}}>
<CursorImage />
</div>
)
}
We'll build the avatar cursors later, let's hook everything up first!
Liveblocks Concepts
Liveblocks makes use of a couple of concepts that we must understand, the first being a room.
Room
A room, much like real life, is a single location where users can congregate. If you're using live cursors on multiple pages in your app, it makes sense to create a new room for each page. You can also create new rooms for the same page, when the initial room becomes too busy.
Presence
Presence refers to the movements and actions of people within the rooms. In the case of our demo, presence will refer to an object held by each user, containing the locations of their cursors on the page.
Rendering the cursors
The next step is to render the cursors to the screen. For this, we're going to create a new component named CursorPresence
. I'll
explain in detail below, but first, here's the code:
import { useUpdateMyPresence, useOthers } from '@liveblocks/react'
import Cursor from './Cursor.js'
export default function CursorPresence ({ children }) {
const updateMyPresence = useUpdateMyPresence()
const onPointerMove = event => {
updateMyPresence({
cursor: {
x: Math.round(event.clientX),
y: Math.round(event.clientY)
}
})
}
const onPointerLeave = () => {
updateMyPresence({ cursor: null })
}
const others = useOthers()
const showOther = ({ connectionId, presence }) => {
if (!presence || !presence.cursor) {
return null
}
const { x, y } = presence.cursor
return (
<Cursor key={connectionId} x={x} y={y} />
)
}
return (
<div onPointerMove={onPointerMove} onPointerLeave={onPointerLeave}>
{others.map(showOther)}
{children}
</div>
)
}
This will make sense in a minute! Let's break it down.
useUpdateMyPresence
useUpdateMyPresence()
is a function that updates the current user's presence, and automagically sends out updates to
other connected users. We're using it within onPointerMove
to pass the current location of the cursor, and then
within onPointerLeave
to reset the location if the cursor leaves the element.
// Update your cursor locations with this function
const updateMyPresence = useUpdateMyPresence()
// When your cursor moves, update your presence with its current location
const onPointerMove = event => {
updateMyPresence({
cursor: {
x: Math.round(event.clientX),
y: Math.round(event.clientY)
}
})
}
// If your cursor leaves the element, or window, set cursor to null
const onPointerLeave = () => {
updateMyPresence({ cursor: null })
}
...
return (
<div onPointerMove={onPointerMove} onPointerLeave={onPointerLeave}>
...
</div>
)
Now we're successfully updating our presence/cursor location!
useOthers
useOthers()
is a function that returns an array of each user's presence (excluding yours). We can utilise this to display
other users' cursors. We'll create a function that renders the cursor to the page, so long as presence
and
presence.cursor
are set, and call this on each user's presence.
...
// Get other users' presence
const others = useOthers()
// Function to display a user's presence
const showOther = ({ connectionId, presence }) => {
// If presence or cursor null or undefined, don't display
if (!presence || !presence.cursor) {
return null
}
// Display cursor
const { x, y } = presence.cursor
return (
<Cursor key={connectionId} x={x} y={y} />
)
}
return (
<div onPointerMove={onPointerMove} onPointerLeave={onPointerLeave}>
{others.map(showOther)}
{children}
</div>
)
Note that we've used children
within the div
; this is because we'll be placing our entire page within this component, to allow
cursors to be seen across the whole page. We're also using connectedId
, which is a handy unique id we use as a React key
.
Here's the entire component again, with added comments:
CursorPresence.js
import { useUpdateMyPresence, useOthers } from '@liveblocks/react'
import Cursor from './Cursor.js'
export default function CursorPresence ({ children }) {
// Update your cursor locations with this
const updateMyPresence = useUpdateMyPresence()
// When your cursor moves, update your presence with its current location
const onPointerMove = event => {
updateMyPresence({
cursor: {
x: Math.round(event.clientX),
y: Math.round(event.clientY)
}
})
}
// If your cursor leaves the element, or window, set presence to null
const onPointerLeave = event => {
updateMyPresence({ cursor: null })
}
// Get other users' presence
const others = useOthers()
// Display another's presence
const showOther = ({ connectionId, presence }) => {
// If presence is not set or cursor location is null, don't display
if (!presence || !presence.cursor) {
return null
}
// Display cursor
const { x, y } = presence.cursor
return (
<Cursor key={connectionId} x={x} y={y} />
)
}
return (
<div onPointerMove={onPointerMove} onPointerLeave={onPointerLeave}>
{others.map(showOther)}
{children}
</div>
)
}
On to the final step now, adding the room!
Creating the room
We can create a file, /components/Room.js
, to serve as a Room for our project. All we need to do is place
our newly built CursorPresence
component within RoomProvider
, and pass a default presence.
import { RoomProvider } from '@liveblocks/react'
import CursorPresence from './CursorPresence'
export default function Room ({ children }) {
const defaultPresence = () => ({
cursor: null
})
// Creating a room
return (
<RoomProvider id="live-cursors" defaultPresence={defaultPresence}>
<CursorPresence>
{children}
</CursorPresence>
</RoomProvider>
)
}
Following on from earlier, the default presence is { cursor: null }
, to represent a cursor that isn't being rendered.
Now we can load Room
into our chosen page, pass any body content within its tags, and our page should be rendered:
import Room from '../components/Room'
export default function Index () {
return (
<Room>
Page content here
</Room>
)
}
And that's it! We now have working live cursors. Here's a complete example on CodeSandbox:
An extra touch
With a couple of little changes we can add some fancy cursors, and make sure that everyone sees the correct image.
Edit API
Any data passed to authorize({ userInfo })
, within /pages/api/auth.js
, will persist while the user is connected to the platform.
We can generate a number that represents an avatar, use this object to store it, and then pass this to each other user. This means that
if a user is given avatar-3.svg
, all other users will see avatar-3.svg
for that user, not just a random avatar. Here's the code before & after:
const result = await authorize({ room, secret })
const result = await authorize({
room,
secret,
userInfo: {
// There are 59 avatars in the set, pick a number at random
avatarId: Math.floor(Math.random() * 58) + 1
}
})
Edit Cursor
Now we can add avatarId
to the Cursor
props, which can be used to grab a file, and style the new cursor:
export default function Cursor ({ x = 0, y = 0 }) {
}
return (
<div style={...}>
<CursorImage />
</div>
)
export default function Cursor ({ x = 0, y = 0, avatarId }) {
}
return (
<div style={...}>
{/* Style new cursor */}
<img src={`/path/to/images/avatar-${avatarId}.svg`} style={...} alt="..." />
</div>
)
Edit CursorPresence
And finally, pass the id to the cursor within CursorPresence
using the info
property:
// Function to display a user's presence
const showOther = ({ connectionId, presence }) => {
...
return (
<Cursor key={connectionId} x={x} y={y} />
)
}
// Function to display a user's presence
const showOther = ({ connectionId, presence, info }) => {
...
return (
<Cursor key={connectionId} x={x} y={y} avatarId={info.avatarId} />
)
}
Fancy cursors complete!
Summary
It's now amazingly easy to create collaborative experiences thanks to Liveblocks. And bear in mind that any data can be sent as presence, not just cursor positions. We've only reached the tip of the iceberg in 2021—collaboration is our future!