Skip to content
Article posted on

Live Cursors with Liveblocks & Next.js
How to display users' live cursors with Liveblocks, Next.js, & Vercel

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!
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!
Simulated cursors

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:

Edit on CodeSandbox
Live Cursors with Liveblocks & Next.js, initial template

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:

.env.local
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:

/pages/api/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:

/pages/_app.js
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.

/components/Cursor.js
// 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.

Room
Live Cursors with Liveblocks & Next.jsMonday, 2 July 2021Display users' live cursors with Liveblocks, Next.js, & VercelDisplaying 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!Need to knowIn this article I'll be assuming you have a basic understanding of Next.js& Vercel.What is Liveblocks?Presencecursor: { x: 1338, y: 687,}connectionId: 26Presencecursor: { x: 254, y: 474,}connectionId: 32

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:

/components/CursorPresence.js
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.

/components/Room.js
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:

/pages/index.js
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:

Edit on CodeSandbox
Live Cursors with Liveblocks & Next.js, complete

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:

Before (fragment within /pages/api/auth.js)
const result = await authorize({ room, secret })
After (fragment within /pages/api/auth.js)
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:

Before (fragment within /components/Cursor.js)
export default function Cursor ({ x = 0, y = 0 }) {
}
return (
  <div style={...}>
    <CursorImage />
  </div>
)
After (fragment within /components/Cursor.js)
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:

Before (fragment within /components/CursorPresence.js)
// Function to display a user's presence
const showOther = ({ connectionId, presence }) => {
  ...
  return (
    <Cursor key={connectionId} x={x} y={y} />
  )
}
After (fragment within /components/CursorPresence.js)
// 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!