Skip to content
Roomful is in public beta — install with the @beta tag. Share feedback →

UI Components (`@roomful/cursors`)

Audience: users.

Storybook: https://erayates.github.io/roomful/storybook/

ComponentPurpose
PeerCursoranimated cursor with label
PresenceBaronline user list
PresenceAvatarscompact avatar stack
LiveIndicatorarea-level activity marker
TypingIndicatortyping state visualization
CollaborationBadgepeer activity indicator on element
SelectionHighlightpeer selection overlay
FloatingReactiontransient reaction animation
import { PresenceBar } from '@roomful/cursors';
import { RoomfulProvider } from '@roomful/react';
export function HeaderPresence() {
return (
<RoomfulProvider
roomId="presence-demo"
presence={{ avatar: '/avatars/ada.png', color: '#4F46E5', name: 'Ada Lovelace' }}
transport="broadcast"
>
<PresenceBar
maxVisible={5}
showNames
size="md"
onUserClick={(user) => console.log(user.id)}
/>
</RoomfulProvider>
);
}
PropTypeNotes
maxVisiblenumbervisible peer chips before rendering a separate +N overflow chip
showNamesbooleanshows or hides inline peer names; defaults to true
size'sm' | 'md' | 'lg'controls avatar, chip, and text sizing
onUserClick(peer: Peer) => voidmakes peer chips interactive and receives the clicked peer

PresenceBar reads peers from usePresence().all, includes the local user, and uses native title tooltips for full names. Avatars come from peer.avatar; when absent, the chip shows colored initials using peer.color or a deterministic fallback color derived from peer.id.

import { PresenceAvatars } from '@roomful/cursors';
import { RoomfulProvider } from '@roomful/react';
export function CompactPresence() {
return (
<RoomfulProvider roomId="presence-demo" presence={{ color: '#0F766E', name: 'Grace Hopper' }}>
<PresenceAvatars maxVisible={3} onUserClick={(user) => console.log(user.name)} />
</RoomfulProvider>
);
}
PropTypeNotes
maxVisiblenumbervisible avatar circles before rendering a stacked +N badge
size'sm' | 'md' | 'lg'controls avatar and overflow badge sizing
onUserClick(peer: Peer) => voidmakes avatar circles interactive and receives the clicked peer

PresenceAvatars uses the same peer data and avatar fallback rules as PresenceBar, but renders an overlapping stack of avatar circles for dense header layouts.

import { TypingIndicator } from '@roomful/cursors';
export function ComposerFooter({ peers }) {
return <TypingIndicator peers={peers} ariaLabel="Users currently typing" />;
}
PropTypeNotes
peersPeer[]peers currently typing; renders nothing when the array is empty
ariaLabelstringoverrides the default accessible label text

TypingIndicator renders inline text plus three animated CSS dots. It shows up to three peer names and collapses any remainder into and N others.

import { LiveIndicator } from '@roomful/cursors';
export function LivePresenceBadge() {
return <LiveIndicator color="#f97316" size={12} ariaLabel="Live editing hotspot" />;
}
PropTypeNotes
colorstringpulse and core color; defaults to green
sizenumberrendered diameter in pixels
ariaLabelstringoverrides the default accessible label text

LiveIndicator renders a compact pulsing dot for presence hotspots and uses inline styles plus embedded keyframes only.

import { CollaborationBadge } from '@roomful/cursors';
export function FieldBadge({ peer }) {
return (
<div style={{ position: 'relative' }}>
<textarea aria-label="Collaborative field" />
<CollaborationBadge peer={peer} position={{ right: 8, top: 8 }} />
</div>
);
}
PropTypeNotes
peerPeersupplies the badge label and color fallback
position{ top?: number | string; right?: number | string; bottom?: number | string; left?: number | string }absolute offsets inside a relatively positioned parent

CollaborationBadge renders a decorative absolute-positioned badge for peers actively editing an element. It uses peer.name when available, falls back to peer.id, and derives the badge color from peer.color or the same deterministic fallback palette used by the presence components.

import { PeerCursor } from '@roomful/cursors';
{
cursors.map((cursor) => (
<PeerCursor
key={cursor.userId}
x={cursor.x}
y={cursor.y}
name={cursor.name}
color={cursor.color}
idle={cursor.idle}
style="arrow"
/>
));
}
PropTypeNotes
xnumbernormalized horizontal position from 0 to 1
ynumbernormalized vertical position from 0 to 1
namestringpeer label rendered next to the cursor
colorstringmarker color and label background color
idlebooleanstarts a 3 second label hide timer when true
style'arrow' | 'dot' | 'pointer'selects one of the built-in inline SVG markers

PeerCursor uses inline styles only, sets aria-hidden="true", and keeps movement interpolation inside the component with CSS transitions. When idle becomes true, the marker stays visible and the name label fades out after 3 seconds of inactivity.

import { SelectionHighlight } from '@roomful/cursors';
export function RemoteSelection({ peer }) {
return (
<>
<p id="editor-copy">Ada Lovelace writes tests.</p>
<SelectionHighlight peer={peer} selection={{ elementId: 'editor-copy', from: 4, to: 12 }} />
</>
);
}
PropTypeNotes
peerPeersupplies the highlight color fallback
selection{ elementId: string; from: number; to: number } | nulltargets a text range inside the element identified by elementId

SelectionHighlight prefers the CSS Custom Highlight API and falls back to span injection when highlights are unavailable. The component normalizes reversed ranges, clamps invalid offsets, and removes any injected styles or wrapper spans when the selection changes or the component unmounts.

import { FloatingReaction } from '@roomful/cursors';
export function ReactionBurst() {
return (
<div style={{ position: 'relative', width: '180px', height: '180px' }}>
<FloatingReaction emoji="🔥" x={0.45} y={0.7} size={40} />
</div>
);
}

Provide x and y as normalized values (0 to 1) so you can anchor reactions to cursor data, and use delayMs to stagger multiple reactions at the same spot. The component floats upward, fades out over the default 1.5s, and removes itself once the animation completes.

  • Use kit components for faster integration.
  • Use custom renderers for product-specific interaction design.