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.
React / Next.js
Other Frameworks
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>
);
<!-- After initializing Velt client -->
<script type="module">
import { initVelt } from '@veltdev/client';
let client;
(async () => {
client = await initVelt('YOUR_API_KEY');
const liveStateSyncElement = client.getLiveStateSyncElement();
liveStateSyncElement.enableSingleEditorMode({
customMode: false,
singleTabEditor: true
});
// Optional default UI
liveStateSyncElement.enableDefaultSingleEditorUI();
// Optional: Scope to containers
liveStateSyncElement.singleEditorModeContainerIds(['editor', 'rightPanel']);
})();
</script>
<!-- Optional built-in UI panel -->
<velt-single-editor-mode-panel shadow-dom="false"></velt-single-editor-mode-panel>
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.
React / Next.js
Other Frameworks
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();
}
<script type="module">
// Assume Velt client is already initialized as shown above
const liveStateSyncElement = Velt.getLiveStateSyncElement();
// 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();
}
</script>
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.
React / Next.js
Other Frameworks
// 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>
);
<script type="module">
const liveStateSyncElement = Velt.getLiveStateSyncElement();
// Reflect viewer vs editor state in UI
const subscription = liveStateSyncElement.isUserEditor().subscribe((userEditorAccess) => {
const role = userEditorAccess?.isEditor ? 'Editor' : 'Viewer';
console.log('Role:', role, 'Editor on current tab:', userEditorAccess?.isEditorOnCurrentTab);
});
</script>
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.
React / Next.js
Other Frameworks
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>
);
<script type="module">
const liveStateSyncElement = Velt.getLiveStateSyncElement();
// Default UI (recommended)
liveStateSyncElement.enableDefaultSingleEditorUI();
// Custom UI: Editor-side — subscribe to requests and accept/reject
liveStateSyncElement.isEditorAccessRequested().subscribe((data) => {
if (data) {
const nameOrEmail = data.requestedBy?.name || data.requestedBy?.email;
console.log(`Access requested by: ${nameOrEmail}`);
// Example: show banner with Accept/Reject buttons that call:
// liveStateSyncElement.acceptEditorAccessRequest();
// liveStateSyncElement.rejectEditorAccessRequest();
}
});
// Custom UI: Viewer-side — request/cancel access
function requestAccess() {
liveStateSyncElement.requestEditorAccess().subscribe((status) => {
console.log('Request status:', status);
});
}
function cancelAccessRequest() {
liveStateSyncElement.cancelEditorAccessRequest();
}
</script>
<!-- Example buttons:
<button onclick="requestAccess()">Request edit access</button>
<button onclick="cancelAccessRequest()">Cancel request</button>
-->
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.
React / Next.js
Other Frameworks
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>
}
<script type="module">
const liveStateSyncElement = Velt.getLiveStateSyncElement();
liveStateSyncElement.isUserEditor().subscribe((userEditorAccess) => {
if (userEditorAccess?.isEditor && userEditorAccess?.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:
// liveStateSyncElement.editCurrentTab();
}
});
</script>
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:
- Open the same document as two different users in separate browser profiles (or two windows with different auth states).
- Try editing simultaneously. Only one user should be able to edit; the other stays read-only.
- 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
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