Live Cursors
Show who is on a shared surface and where they point: cursors, names, chat bubbles and selections, fed by your own realtime channel.
import { LiveCursorPosition, LiveCursorRect, LiveCursors, LiveCursorsInputOptions, LiveCursorUser, PresenceAvatars } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use live cursors on anything several people work on at once: a design canvas, a whiteboard, a shared document, a dashboard being reviewed together. Youj has no transport of its own: you send the local pointer over whatever you already use (WebSockets, WebRTC, Liveblocks, Supabase Realtime, a CRDT's awareness) and pass everyone else back in.
import { LiveCursors, PresenceAvatars, useLiveCursorsInput, type LiveCursorUser } from '@youj/react'
const surface = useRef<HTMLDivElement>(null)
const [others, setOthers] = useState<LiveCursorUser[]>([])
const [following, setFollowing] = useState<string | null>(null)
// Send your pointer: { x, y } in fractions of the surface, or null when it leaves.
useLiveCursorsInput(surface, (position) => channel.send({ id: me.id, name: me.name, ...position }))
// Receive everyone else's into `others` (leave yourself out).
useEffect(() => channel.onPresence(setOthers), [])
<PresenceAvatars users={others} following={following} onFollowingChange={setFollowing} />
<LiveCursors ref={surface} users={others} following={following}>
<Canvas />
</LiveCursors>LiveCursors wraps the surface (it renders a relative box around its children) and draws a cursor for each person in users with an x and y. Positions are fractions of the surface, 0 to 1 from the top left, so a person on a wide screen and one on a phone point at the same spot. Leave x and y out while someone's pointer is elsewhere and their cursor goes; remove them from users when they leave.
Each person has a color, by default one of the theme's five chart colors picked from their id, so every screen gives them the same one. Five colors mean two people can share one; pass color (a slot your server hands out, say) where they must differ. The name tag's text turns black or white for contrast. A message shows as a chat bubble under the name, and selection draws their selected areas as tinted rectangles.
Updates can be sparse: each cursor springs to its newest position, all of them on one animation loop that stops when they rest or the surface scrolls out of view. A cursor that hasn't moved for idleAfter milliseconds (10 seconds by default; 0 keeps them) fades out until it moves or speaks again.
useLiveCursorsInput(ref, onMove, { throttle }) reports the pointer at most every throttle milliseconds (50 by default), always ending on the last move, and null when it leaves the surface.
PresenceAvatars shows who is here, with each person's color as a ring and their status ("Viewing" or "Editing") in a tooltip. Give it following and onFollowingChange and each avatar becomes a button that follows that person: pass the same following to LiveCursors, which scrolls their cursor into view as they move. Without them, the avatars only show who's here. Past max (5) the rest are counted.
Description
Three simulated people in a design file: two editing, one typing a comment. Move your pointer over the canvas: it goes out through useLiveCursorsInput and comes straight back as the "You" cursor.
Updates arrive a few times a second, as they would over a socket, and each cursor springs between them.
A board larger than its view. Press Ada's avatar to follow her: her cursor is scrolled into view as she moves. Press it again to stop.
Code
live-cursors-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsusers, following, idleAfter… (17)
Props
LiveCursors
| Prop | Type | |
|---|---|---|
usersRequired | readonly LiveCursorUser[]Everyone else on the surface. Leave the local person out. | – |
following | string | nullThe id of a person to follow: their cursor is scrolled into view as it moves. Pair it with PresenceAvatars' | – |
idleAfter | numberMilliseconds without a move (or a new message) before a cursor fades out. default DEFAULT_IDLE | DEFAULT_IDLE |
announce | booleanAnnounce joins and leaves in a polite live region. default true | true |
locale | stringFormats the announced lists of names. Default: the YoujProvider locale. | – |
Other props pass through: extends ComponentProps<'div'>. | ||
PresenceAvatars
| Prop | Type | |
|---|---|---|
usersRequired | readonly LiveCursorUser[]Everyone here, in the order to show them. | – |
max | numberAvatars shown before "+N". default 5 | 5 |
size | 'sm' | 'default' | 'lg'default 'default' | 'default' |
following | string | nullThe id of the person being followed, or | – |
defaultFollowing | string | nullThe person followed at first. default null | null |
onFollowingChange | (id: string | null) => voidCalled with an id when an avatar is pressed, and | – |
label | stringThe group's name. Default: the labels' "People here". | – |
Other props pass through: extends ComponentProps<'div'>. | ||
LiveCursorUser
| Field | Type | |
|---|---|---|
id | string | Stable across updates: it keys the cursor and picks the color. |
name | string | The name tag. |
x, y | number | Where their pointer is, as fractions of the surface. Leave both out while it's elsewhere. |
color | string | Any CSS color. Default: a chart color chosen by id. |
message | string | A chat bubble under the name. |
selection | LiveCursorRect[] | Selected areas, { x, y, width, height } in fractions. |
status | 'viewing' | 'editing' | Shown in PresenceAvatars' tooltip. |
src | string | An avatar image. |
StylingParts and data attributes
Styling
Parts: live-cursors (with data-yj-following while a placed person is followed), live-cursors-overlay, live-cursor (with data-yj-idle once faded and data-yj-following), live-cursor-name, live-cursor-message, live-cursor-selection, live-cursors-follow-anchor, presence-avatars, presence-avatar (with data-status: viewing or editing, and aria-pressed while followed) and presence-avatars-more. Each cursor, selection and avatar sets --ui--live-cursor to the person's color. The overlay is left to right whatever the page's direction: a position is the same spot on every screen.
MotionGlide, enter, leave, idle · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Cursor moveseach cursor springs to its newest position, all on one animation loop that stops when they rest or the surface is off screen | spring.snappy0.2s, bounce 0 | 0.2s, bounce 0 | |
| Someone arrives or leavesthe cursor scales up from its tip, or shrinks away | duration.fastease.enter140ms · cubic-bezier(0.16, 1, 0.3, 1) | 140mscubic-bezier(0.16, 1, 0.3, 1) | |
| Chat bubblepops in under the name and out when the message clears | duration.fastease.enter140ms · cubic-bezier(0.16, 1, 0.3, 1) | 140mscubic-bezier(0.16, 1, 0.3, 1) | |
| Idlea cursor that stops moving fades out after idleAfter | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) |
Reduced motion
Cursors jump to each update, and appear, go idle and leave at once.
Reduced motion
Each cursor jumps to its newest position; cursors, bubbles and selections appear and go without fades or scaling, and following scrolls without smoothing.
AccessibilityHidden cursors, announcements, avatars
Accessibility
- The cursors, bubbles and selections are decorative and hidden from screen readers; the surface's own content stays as it is.
- A polite live region announces who joined and who left ("Joan and Kenji joined. Grace left"), not who was already there. Turn it off with
announce={false}where the app announces presence itself. - PresenceAvatars is a group named "People here" (or
label). With following, each avatar is a toggle button named "Follow Ada", pressed while followed, and starting to follow is announced ("Following Ada"); without it, each is an image named with the person and their status ("Ada, Editing"). - Its words come from the labels (
liveCursorsJoined,liveCursorsLeft,liveCursorsPeople,liveCursorsViewing,liveCursorsEditing,liveCursorsFollow,liveCursorsFollowingandliveCursorsMore), translated by the language packs; lists of names are formatted by Intl in the locale.