Sign in

Forms

Create a multiplayer form with Liveblocks, for onboarding flows, RFP responses, intake questionnaires, or any document your users fill in together. Synchronize every field value, show who’s editing which field, prefill answers from your back end, and let AI complete fields alongside your users.

Forms

Collaboration in the Multiplayer form example

Features

Get started

Choose a starting point for your form.

Implementation

This is an overview of how each feature can be implemented. Store permanent field values in Sync. Keep temporary focus and selection state in Presence. Use Comments for review discussions on individual fields.

Realtime collaboration

Store the form’s answers in a LiveObject, one property per field. Changes to different fields merge automatically, so two people can fill in different parts of the form at the same time without overwriting each other. Read values with useStorage and update them with useMutation.

import { useMutation, useStorage } from "@liveblocks/react/suspense";
function CompanyField() { const company = useStorage((root) => root.fields.company); const updateField = useMutation(({ storage }, value: string) => { storage.get("fields").set("company", value); }, []);
return ( <input value={company} onChange={(e) => updateField(e.target.value)} /> );}

For long answers where several people may type in the same field simultaneously, use LiveText so concurrent keystrokes merge instead of replacing the whole value. Learn more under Storage.

Live field presence

Show which field each collaborator is focusing, so people naturally avoid typing in the same input. Focus is temporary, so store it in Presence—publish it with useUpdateMyPresence and read collaborators with useOthers.

import { useOthers, useUpdateMyPresence } from "@liveblocks/react/suspense";
function FormField({ fieldId }: { fieldId: string }) { const updateMyPresence = useUpdateMyPresence(); const others = useOthers(); const editor = others.find( (other) => other.presence.focusedFieldId === fieldId );
return ( <div style={{ outline: editor ? `2px solid ${editor.info.color}` : "" }}> <input onFocus={() => updateMyPresence({ focusedFieldId: fieldId })} onBlur={() => updateMyPresence({ focusedFieldId: null })} /> {editor ? <span>{editor.info.name} is editing</span> : null} </div> );}

Add AvatarStack at the top of the form to show everyone currently filling it in, and configure resolveUsers to provide their names and colors.

Server-side editing

Trusted server processes can prefill or update the form with Liveblocks.mutateStorage, for example filling in known answers from your CRM when the form is created. The server writes the same Sync data as connected users, so prefilled values appear in realtime.

import { Liveblocks } from "@liveblocks/node";
const liveblocks = new Liveblocks({ secret: process.env.LIVEBLOCKS_SECRET_KEY!,});
await liveblocks.mutateStorage("form-room", ({ root }) => { const fields = root.get("fields");
fields.set("company", "Acme Inc."); fields.set("contactEmail", "olivier@acme.inc");});

Learn more under Server-side editing.

Agentic editing

To let AI complete the form, generate validated values with AI, then use the same mutation shown under Server-side editing to apply them. Use setPresence before and after generation so the agent appears in Presence alongside humans, focusing fields as it fills them in. Finally, remove the agent’s presence to indicate that it’s no longer working.

import { Liveblocks } from "@liveblocks/node";import { generateText, Output } from "ai";import { z } from "zod";
const liveblocks = new Liveblocks({ secret: process.env.LIVEBLOCKS_SECRET_KEY!,});
const roomId = "form-room";const agent: Liveblocks["UserMeta"] = { id: "ai-agent", info: { name: "AI agent", color: "#7c3aed" },};
await liveblocks.setPresence(roomId, { userId: agent.id, userInfo: agent.info, data: { status: "thinking", focusedFieldId: "summary" }, ttl: 60,});
await liveblocks.mutateStorage(roomId, async ({ root }) => { const fields = root.get("fields");
const { output } = await generateText({ model: "openai/gpt-5.6-sol", output: Output.object({ schema: z.object({ summary: z.string(), industry: z.string(), }), }), prompt: `Complete the remaining fields of this intake form. Here are the current answers: ${fields.toJSON()}`, });
fields.set("summary", output.summary); fields.set("industry", output.industry);});
await liveblocks.setPresence(roomId, { userId: agent.id, userInfo: agent.info, data: { status: "idle", focusedFieldId: null }, ttl: 2,});

Additionally, you can use Feeds to store AI workflow state, and to pass agent status updates to the UI. Learn more under Agentic editing.

Multiplayer undo/redo

Connect undo and redo to your form with useUndo and useRedo. Each user’s history is independent, so undoing your own answer never reverses a change made by someone else.

import {  useCanRedo,  useCanUndo,  useRedo,  useUndo,} from "@liveblocks/react/suspense";
function FormToolbar() { const undo = useUndo(); const redo = useRedo(); const canUndo = useCanUndo(); const canRedo = useCanRedo();
return ( <> <button onClick={undo} disabled={!canUndo}> Undo </button> <button onClick={redo} disabled={!canRedo}> Redo </button> </> );}

Learn more under Multiplayer undo/redo.

Comments

Reviewers often need to discuss an answer before it’s final. Attach Comments to a field by storing its ID in thread metadata, create threads with Composer, and render each field’s discussion next to it with useThreads.

import { useThreads } from "@liveblocks/react/suspense";import { Composer, Thread } from "@liveblocks/react-ui";
function FieldComments({ fieldId }: { fieldId: string }) { const { threads } = useThreads({ query: { metadata: { fieldId } } });
return ( <aside> {threads.map((thread) => ( <Thread key={thread.id} thread={thread} /> ))} <Composer metadata={{ fieldId }} /> </aside> );}

Learn more under the comments use case.

Permissions

Each form is contained inside a room in your Liveblocks app, and permission groups can set access to it. For example, your form may have an editor group that fills it in and a viewer group that can only review and comment. This can be set when modifying or creating a room, for example with Liveblocks.createRoom.

await liveblocks.createRoom(`my-room-id`, {  defaultAccesses: [    // No access by default  ],  groupsAccesses: {    // "reviewers" group can read the form and leave comments    reviewers: ["*:read", "comments:write"],  },  usersAccesses: {    // "olivier" can fill in the form    olivier: ["*:write"],  },});

More complex controls can be set too, learn more under Permissions.

Examples

Explore complete examples that combine the features described above.