Prerequisites

  • Node.js (v14 or higher)
  • React (v16.8 or higher for hooks)
  • A Velt account with an API key (sign up)
  • Optional: TypeScript for type safety

Setup

Step 1: Setup Velt

Setup and initialize the Velt client by following the Velt Setup Docs. This is required for Single Editor Mode to work.

Step 2: Initialize Single Editor Mode

  • Get the Live State Sync element via hook or client API.
  • Enable Single Editor Mode. Optionally pass config:
    • customMode: When true, SDK won’t automatically make HTML elements read-only for viewers. You must manage UI state manually or using the APIs listed here. Default: false
    • singleTabEditor: Restrict editing to a single browser tab for the editor. Default: true
  • Optionally enable/disable the default UI panel. Default: enabled
  • Optionally scope the feature to specific containers.
  • We recommend using the default UI for simplicity and velocity. You can customize it to change the look, feel and position of the UI. You can customize the UI by following this guide.
  • If you do want to create your UI from scratch then you can use the following APIs to create a similar UX: Editor and Viewer.
  const liveStateSyncElement = useLiveStateSyncUtils();

  useEffect(() => {
    if (liveStateSyncElement) {
      // Enable Single Editor Mode
      liveStateSyncElement.enableSingleEditorMode({
        customMode: false,
        singleTabEditor: true
      });

      // Optional: Show default UI panel
      liveStateSyncElement.enableDefaultSingleEditorUI();

      // Optional: Restrict to specific containers
      liveStateSyncElement.singleEditorModeContainerIds(['editor', 'rightPanel']);
    }

    // Cleanup on unmount
    return () => liveStateSyncElement.disableSingleEditorMode();
  }, [liveStateSyncElement]);

  return (
    <div id="editor">
      {/* Single Editor Mode Panel UI */}
      <VeltSingleEditorModePanel shadowDom={false} />
    </div>
  );

Step 3: Set the editor

  • Declare the editor for the current document/session so editing is enabled for one user while others remain read-only.
  • You can set the current user as the editor programmatically on an explicit action.
const liveStateSyncElement = useLiveStateSyncUtils();

// Programmatically set the current user as the editor when they perform an explicit action. eg: start typing or interact with the UI.
function setEditor() {
  liveStateSyncElement?.setUserAsEditor();
}
When the editor is set, editing tools should become enabled for that user while other users are placed in read-only mode. You can fine tune which elements are enabled/disabled by following this guide.
Learn more: Set User as Editor

Step 4: Update UI to reflect editor and viewer states

  • Non-editor users are automatically viewers. Reflect that state in your UI.
  • If you are using the default UI, it will automatically reflect the editor and viewer states in the UI. You can customize it to change the look, feel and position of the UI.
// Reflect viewer vs editor state in UI
const { isEditor, isEditorOnCurrentTab } = useUserEditorState();

return (
  <div>
    <div>Role: {isEditor ? 'Editor' : 'Viewer'}</div>
    {isEditor && <div>Editing on this tab: {isEditorOnCurrentTab ? 'Yes' : 'No'}</div>}
  </div>
);
See: Is User Editor

Step 5: Passing access (requests + accept/reject UX)

Provide an access handoff flow so viewers can request edit access and the editor can accept or reject.
  • Default UI (recommended): Use the built-in banner/controls.
  • Custom UI: Use the APIs to request and respond to access.
const liveStateSyncElement = useLiveStateSyncUtils();

// Default UI (recommended): ensure default UI is enabled and/or panel is rendered
liveStateSyncElement.enableDefaultSingleEditorUI();
// <VeltSingleEditorModePanel shadowDom={false} />

// Custom UI: Editor-side — show banner and accept/reject
const editorAccessRequested = useEditorAccessRequestHandler();

return (
  <div>
    {editorAccessRequested && (
      <div>
        {`User ${editorAccessRequested.requestedBy?.name || editorAccessRequested.requestedBy?.email} requests access`}
        <button onClick={() => liveStateSyncElement.acceptEditorAccessRequest()}>Accept</button>
        <button onClick={() => liveStateSyncElement.rejectEditorAccessRequest()}>Reject</button>
      </div>
    )}

    {/* Custom UI: Viewer-side — request/cancel access */}
    <button
      onClick={() =>
        liveStateSyncElement.requestEditorAccess().subscribe((status) => {
          console.log('Request status:', status);
        })
      }
    >
      Request edit access
    </button>
    <button onClick={() => liveStateSyncElement.cancelEditorAccessRequest()}>Cancel request</button>
  </div>
);
Learn more: Is Editor Access Requested, Accept Editor Access Request, Reject Editor Access Request, Request Editor Access, Cancel Editor Access Request

Step 6: Prevent concurrent editing by the same user (tab locking)

If the same user opens another tab and tries to edit, lock the editor to a single tab and guide them back to the active tab when needed.
const liveStateSyncElement = useLiveStateSyncUtils();
const { isEditor, isEditorOnCurrentTab } = useUserEditorState();

// If the user is the editor but on the wrong tab, prompt and switch
if (isEditor && isEditorOnCurrentTab === false) {
  // Show UX prompt like: "You are already editing this page in another tab. Continue editing there."
  // Provide an action to move editing to this tab:
  const takeOver = () => liveStateSyncElement.editCurrentTab();
  // Example: <button onClick={takeOver}>Edit on this tab</button>
}
Recommended UX copy: “You are already editing this page in another tab. Continue editing there.”
Learn more: Edit Current Tab and Is User Editor

Notes

  • Initialize Velt first: Ensure the Velt client is initialized and the user/document are set before enabling Single Editor Mode.
  • Default UI: The built-in UI can be shown with enableDefaultSingleEditorUI() and/or by rendering <VeltSingleEditorModePanel /> (React) or <velt-single-editor-mode-panel> (HTML).
  • Restrict scope: Use singleEditorModeContainerIds([...]) to limit Single Editor Mode to specific containers instead of the entire DOM.
  • Native elements only: When customMode: true, you must mark native HTML elements with attributes to control them:
    • data-velt-sync-access="true" to enable control
    • data-velt-sync-access-disabled="true" to exclude elements
    • These attributes work on native elements only, not directly on React components.

Testing and Debugging

To test Single Editor Mode:
  1. Open the same document as two different users in separate browser profiles (or two windows with different auth states).
  2. Try editing simultaneously. Only one user should be able to edit; the other stays read-only.
  3. Use the default UI panel to request/transfer access and verify the flow.
Common issues:
  • UI not visible: Ensure enableDefaultSingleEditorUI() is called or the panel component/element is rendered.
  • Elements not disabled: If using customMode: true, ensure you set data-velt-sync-access attributes on native elements. Attributes do not attach to React components.
  • Editor can edit in multiple tabs: Confirm singleTabEditor is true. Use editCurrentTab() to move the editor role to the current tab.

Complete Example

Relevant Code
import React, { useEffect } from 'react';
import {
  VeltProvider,
  useLiveStateSyncUtils,
  VeltSingleEditorModePanel,
  useUserEditorState,
  useEditorAccessRequestHandler
} from '@veltdev/react';

function SingleEditorExample() {
  const liveStateSyncElement = useLiveStateSyncUtils();
  const { isEditor, isEditorOnCurrentTab } = useUserEditorState();
  const editorAccessRequested = useEditorAccessRequestHandler();

  useEffect(() => {
    liveStateSyncElement.enableSingleEditorMode({ singleTabEditor: true });
    liveStateSyncElement.enableDefaultSingleEditorUI();
    return () => liveStateSyncElement.disableSingleEditorMode();
  }, []);

  return (
    <div id="editor">
      <div>Active editor: {isEditor ? 'Yes' : 'No'}</div>
      <div>Editing on this tab: {isEditorOnCurrentTab ? 'Yes' : 'No'}</div>
      {editorAccessRequested && <div>Access requested by: {editorAccessRequested.requestedBy?.email}</div>}
      <VeltSingleEditorModePanel shadowDom={false} />
    </div>
  );
}

export default function App() {
  return (
    <VeltProvider apiKey="YOUR_API_KEY">
      <SingleEditorExample />
    </VeltProvider>
  );
}

Next: Customize Behavior

Learn how to fine-tune Single Editor Mode behavior, timeouts, events, and UI. Go to Customize Behavior