API Reference - @liveblocks/react-tiptap

@liveblocks/react-tiptap provides you with a React plugin that adds collaboration to any Tiptap text editor. It also adds realtime cursors, document persistence on the cloud, comments, and mentions. Read our get started guides to learn more. Use @liveblocks/node-prosemirror for server-side editing.

Setup

To set up your collaborative Tiptap editor, add useLiveblocksExtension to your editor, passing the return value useEditor extension array.

import { useLiveblocksExtension } from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension();
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
return ( <div> <EditorContent editor={editor} /> </div> );}

Liveblocks Tiptap components should be passed editor to enable them.

import {  useLiveblocksExtension,  FloatingComposer,} from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension();
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
return ( <div> <EditorContent editor={editor} /> <FloatingComposer editor={editor} style={{ width: "350px" }} /> </div> );}

Learn more in our get started guides.

Default components

Toolbar

Displays a toolbar, allowing you to change the styles of selected text. You can add content before or after, or the toolbar’s options can be customized. A floating toolbar also exists.

<Toolbar editor={editor} />
Toolbar

Pass your Tiptap editor to use the component. By default, one of the toolbar buttons can create comment threads—to enable this add FloatingComposer and display threads with AnchoredThreads or FloatingThreads.

import {  useLiveblocksExtension,  Toolbar,  FloatingComposer,  FloatingThreads,} from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension();
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
return ( <div> <Toolbar editor={editor} /> <EditorContent editor={editor} /> <FloatingComposer editor={editor} style={{ width: "350px" }} /> <FloatingThreads editor={editor} style={{ width: "350px" }} /> </div> );}

Extending the defaults

You can insert content before the first button and after the last button using before and after. Components such as Toolbar.Button and Toolbar.Toggle can be used to create new buttons.

import { Toolbar } from "@liveblocks/react-tiptap";import { Icon } from "@liveblocks/react-ui";
<Toolbar editor={editor} before={<>I'm at the start</>} after={ <Toolbar.Button name="Help" icon={<Icon.QuestionMark />} shortcut="CMD-H" onClick={() => console.log("help")} /> }/>;

For more complex customization, instead read creating a custom floating toolbar.

Creating a custom toolbar

By passing elements as children, it’s possible to create a fully custom toolbar.

import { Toolbar } from "@liveblocks/react-lexical";import { Editor } from "@tiptap/react";
function CustomToolbar({ editor }: { editor: Editor | null }) { return ( <Toolbar editor={editor}> Hello <strong>world</strong> </Toolbar> );}

Each part of our default toolbar is available as blocks which can be slotted together. This is how the default toolbar is constructed:

import { Toolbar } from "@liveblocks/react-tiptap";import { Editor } from "@tiptap/react";
function CustomToolbar({ editor }: { editor: Editor | null }) { return ( <Toolbar editor={editor}> <Toolbar.BlockSelector /> <Toolbar.SectionInline /> <Toolbar.Separator /> <Toolbar.SectionCollaboration /> </Toolbar> );}

You can mix these default components with any custom ones of your own. Below the Toolbar.SectionHistory component is added alongside some custom buttons created with Toolbar.Button, Toolbar.Toggle, and Icon. The highlight toggle button requires a Tiptap extension.

import { Toolbar } from "@liveblocks/react-tiptap";import { Icon } from "@liveblocks/react-ui";import { Editor } from "@tiptap/react";
function CustomToolbar({ editor }: { editor: Editor | null }) { return ( <Toolbar editor={editor}> <Toolbar.SectionHistory /> <Toolbar.Separator /> <Toolbar.Button name="Help" icon={<Icon.QuestionMark />} shortcut="CMD-H" onClick={() => console.log("help")} /> <Toolbar.Toggle name="Highlight" icon={<div>🖊️</div>} active={editor?.isActive("highlight") ?? false} onClick={() => editor?.chain().focus().toggleHighlight().run()} disabled={!editor?.can().chain().focus().toggleHighlight().run()} /> </Toolbar> );}

To learn more about the different components, read more below.

Props

  • editorEditor | nullRequired

    The Tiptap editor.

  • childrenReactNode

    The content of the toolbar, overriding the default content. Use the before and after props if you want to keep and extend the default content. Any ReactNode or Toolbar.* components work inside.

  • beforeReactNode

    The content to display at the start of the toolbar. Any ReactNode or Toolbar.* components work inside.

  • afterReactNode

    The content to display at the end of the toolbar. Any ReactNode or Toolbar.* components work inside.

Toolbar.Button

A button for triggering actions. The name is displayed in a tooltip. Props such as onClick will be passed to the underlying button element.

import { Toolbar } from "@liveblocks/react-tiptap";
<Toolbar editor={editor}> <Toolbar.Button name="Question" onClick={(e) => console.log("Clicked")} /></Toolbar>;

Optionally takes an icon which will visually replace the name. Also optionally accepts a shortcut, which is displayed in the tooltip. Comment key names are converted to symbols. Here are various examples.

import { Toolbar } from "@liveblocks/react-tiptap";import { Icon } from "@liveblocks/react-ui";
// Button says "Question"<Toolbar.Toggle name="Question" onClick={/* ... */} />
// Tooltip says "Question [⌘+Q]"<Toolbar.Button name="Question" shortcut="CMD+Q" onClick={/* ... */} />
// Custom icon, replaces the name in the button<Toolbar.Button name="Question" icon={<div>?</div>} onClick={/* ... */} />
// Using a Liveblocks icon, replaces the name in the button<Toolbar.Button name="Question" icon={<Icon.QuestionMark />} onClick={/* ... */} />
// Passing children visually replaces the `name` and `icon`<Toolbar.Button name="Question" onClick={/* ... */}> ? Ask a question</Toolbar.Button>
// Props are passed to the inner `button`<Toolbar.Button name="Question" style={{ marginLeft: 10 }} className="custom-button" onMouseOver={() => console.log("Hovered")}/>
Props
  • namestringRequired

    The name of this button displayed in its tooltip. Will also be displayed in the button if no icon or children are passed.

  • iconReactNode

    An optional icon displayed in this button.

  • shortcutstring

    An optional keyboard shortcut displayed in this button’s tooltip. Common shortcuts such will be replaced by their symbols, for example CMD.

Toolbar.Toggle

A toggle button for values that can be active or inactive. Best used with text editor commands. The name is displayed in a tooltip. Props will be passed to the underlying button element.

import { Toolbar } from "@liveblocks/react-tiptap";
<Toolbar editor={editor}> <Toolbar.Toggle name="Highlight" active={editor?.isActive("highlight") ?? false} onClick={() => editor?.chain().focus().toggleHighlight().run()} /></Toolbar>;

The snippet above shows how to use the Toggle with the Tiptap highlight extension. The toggle button can also be toggled with useState.

import { Toolbar } from "@liveblocks/react-tiptap";import { Editor } from "@tiptap/react";import { useState } from "react";
function CustomToggle({ editor }: { editor: Editor | null }) { const [active, setActive] = useState(false);
return ( <Toolbar.Toggle name="Toggle options" active={active} onClick={() => setActive(!active)} /> );}

Toolbar.Toggle optionally takes an icon which will visually replace the name. Also optionally accepts a shortcut, which is displayed in the tooltip. Comment key names are converted to symbols. Here are various examples.

import { Toolbar } from "@liveblocks/react-tiptap";import { Icon } from "@liveblocks/react-ui";
// Button says "Highlight"<Toolbar.Toggle name="Highlight" active={/* ... */} onClick={/* ... */}/>
// Tooltip says "Highlight [⌘+H]"<Toolbar.Toggle name="Highlight" shortcut="CMD+H" active={/* ... */} onClick={/* ... */}/>
// Custom icon, replaces the name in the button<Toolbar.Toggle name="Highlight" icon={<div>🖊</div>} active={/* ... */} onClick={/* ... */}/>
// Using a Liveblocks icon, replaces the name in the button<Toolbar.Toggle name="Highlight" icon={<Icon.QuestionMark />} active={/* ... */} onClick={/* ... */}/>
// Passing children visually replaces the `name` and `icon`<Toolbar.Toggle name="Highlight" active={/* ... */} onClick={/* ... */}> 🖊️Highlight</Toolbar.Toggle>
// Props are passed to the inner `button`<Toolbar.Toggle name="Highlight" active={/* ... */} onClick={/* ... */} style={{ marginLeft: 10 }} className="custom-toggle" onMouseOver={() => console.log("Hovered")}/>
Props
  • namestringRequired

    The name of this button displayed in its tooltip. Will also be displayed in the button if no icon or children are passed.

  • activebooleanRequired

    Whether the button is toggled.

  • iconReactNode

    An optional icon displayed in this button.

  • shortcutstring

    An optional keyboard shortcut displayed in this button’s tooltip. Common shortcuts such will be replaced by their symbols, for example CMD.

Toolbar.BlockSelector

Adds a dropdown selector for switching between different block types, such as text, heading 1, blockquote. Props will be passed to the inner button element. Can also be placed inside FloatingToolbar.

import { Toolbar } from "@liveblocks/react-tiptap";
<Toolbar editor={editor}> <Toolbar.BlockSelector /></Toolbar>;
Use custom item options

If you’d like to change the items shown in the dropdown menu, you can pass a custom items array. Below a code block item (Tiptap extension) is added after the default options.

import { Toolbar } from "@liveblocks/react-tiptap";
<Toolbar editor={editor}> <Toolbar.BlockSelector items={(defaultItems) => [ ...defaultItems, { name: "Code block", icon: <div>❮ ❯</div>, // Optional isActive: (editor) => editor.isActive("codeBlock"), setActive: (editor) => editor.chain().focus().clearNodes().toggleCodeBlock().run(), }, ]} /></Toolbar>;
Customize item styles

By passing a label property, you can overwrite the styles of the dropdown items. The toolbar button will still display the name, but in the dropdown, the label will be used instead of the name and icon. Below, a new item is added and its label is customized.

import { Toolbar } from "@liveblocks/react-tiptap";
<Toolbar editor={editor}> <Toolbar.BlockSelector items={(defaultItems) => [ ...defaultItems, { name: "Code block", label: <div className="font-mono">Code</div>, // Optional, overwrites `icon` + `name` isActive: (editor) => editor.isActive("codeBlock"), setActive: (editor) => editor.chain().focus().clearNodes().toggleCodeBlock().run(), }, ]} /></Toolbar>;

You can also customize the default items. Below each item is styled to represent the effect each block applies to the document.

import { Toolbar } from "@liveblocks/react-tiptap";
<Toolbar.BlockSelector items={(defaultItems) => defaultItems.map((item) => { let label;
if (item.name === "Text") { label = <span>Regular text</span>; }
if (item.name === "Heading 1") { label = ( <span style={{ fontSize: 18, fontWeight: "bold" }}>Heading 1</span> ); }
if (item.name === "Heading 2") { label = ( <span style={{ fontSize: 16, fontWeight: "bold" }}>Heading 2</span> ); }
if (item.name === "Heading 3") { label = ( <span style={{ fontSize: 15, fontWeight: "bold" }}>Heading 3</span> ); }
if (item.name === "Blockquote") { label = ( <span style={{ borderLeft: "3px solid gray", paddingLeft: 8 }}> Blockquote </span> ); }
return { ...item, label, icon: null, // Hide all icons }; }) }/>;
Props
  • itemsarray | function

    The items displayed in this block selector. When provided as an array, the default items are overridden. To avoid this, a function can be provided instead and it will receive the default items.

Toolbar.Separator

Adds a visual, and accessible, separator used to separate sections in the toolbar. Props will be passed to the inner div element. Can also be placed inside FloatingToolbar.

import { Toolbar } from "@liveblocks/react-tiptap";
<Toolbar editor={editor}> <Toolbar.SectionHistory /></Toolbar>;

Toolbar.SectionHistory

Adds a section containing undo and redo buttons. Can also be placed inside FloatingToolbar.

import { Toolbar } from "@liveblocks/react-tiptap";
<Toolbar editor={editor}> <Toolbar.SectionHistory /></Toolbar>;

Toolbar.SectionInline

Adds a section containing inline formatting actions such as bold, italic, underline. Can also be placed inside FloatingToolbar.

import { Toolbar } from "@liveblocks/react-tiptap";
<Toolbar editor={editor}> <Toolbar.SectionInline /></Toolbar>;

Toolbar.SectionCollaboration

Adds a section containing an add comment button. Can also be placed inside FloatingToolbar.

import { Toolbar } from "@liveblocks/react-tiptap";
<Toolbar editor={editor}> <Toolbar.SectionCollaboration /></Toolbar>;

FloatingToolbar

Displays a floating toolbar near the current Tiptap selection, allowing you to change styles. You can add content before or after, or the toolbar’s options can be customized. A static toolbar also exists.

<FloatingToolbar editor={editor} />
FloatingToolbar

Pass your Tiptap editor to use the component. By default, one of the toolbar buttons can create comment threads—to enable this add FloatingComposer and display threads with AnchoredThreads or FloatingThreads.

import {  useLiveblocksExtension,  FloatingToolbar,  FloatingComposer,  FloatingThreads,} from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension();
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
return ( <div> <EditorContent editor={editor} /> <FloatingToolbar editor={editor} /> <FloatingComposer editor={editor} style={{ width: "350px" }} /> <FloatingThreads editor={editor} style={{ width: "350px" }} /> </div> );}

Changing float position

Using position and offset you can reposition the toolbar relative to the current selection. position can be set to "top" or "bottom", and offset defines the vertical distance in pixels from the selection.

<FloatingToolbar  editor={editor}  position="bottom" // Position can be `top` or `bottom`  offset={12} // Distance in px from selection/>

Extending the defaults

You can insert custom content before the first button and after the last button using before and after. Components such as Toolbar.Button and Toolbar.Toggle can be used to create new buttons.

import { Toolbar } from "@liveblocks/react-tiptap";import { Icon } from "@liveblocks/react-ui";
<FloatingToolbar editor={editor} before={<>I'm at the start</>} after={ <Toolbar.Button name="Help" icon={<Icon.QuestionMark />} shortcut="CMD-H" onClick={() => console.log("help")} /> }/>;

For more complex customization, instead read creating a custom floating toolbar.

Creating a custom floating toolbar

By passing elements as children, it’s possible to create a fully custom floating toolbar.

import { FloatingToolbar } from "@liveblocks/react-tiptap";import { Editor } from "@tiptap/react";
function CustomToolbar({ editor }: { editor: Editor | null }) { return ( <FloatingToolbar editor={editor}> Hello <strong>world</strong> </FloatingToolbar> );}

Each part of our default toolbar is available as blocks which can be slotted together. This is how the default floating toolbar is constructed:

import { FloatingToolbar, Toolbar } from "@liveblocks/react-tiptap";import { Editor } from "@tiptap/react";
function CustomToolbar({ editor }: { editor: Editor | null }) { return ( <FloatingToolbar editor={editor}> <Toolbar.BlockSelector /> <Toolbar.SectionInline /> <Toolbar.Separator /> <Toolbar.SectionCollaboration /> </FloatingToolbar> );}

You can mix these default components with any custom ones of your own. Below the Toolbar.SectionHistory component is added alongside some custom buttons created with Toolbar.Button, Toolbar.Toggle, and Icon. The highlight toggle button requires a Tiptap extension.

import { FloatingToolbar, Toolbar } from "@liveblocks/react-tiptap";import { Icon } from "@liveblocks/react-ui";import { Editor } from "@tiptap/react";
function CustomToolbar({ editor }: { editor: Editor | null }) { return ( <FloatingToolbar editor={editor}> <Toolbar.SectionHistory /> <Toolbar.Separator /> <Toolbar.Button name="Help" icon={<Icon.QuestionMark />} shortcut="CMD-H" onClick={() => console.log("help")} /> <Toolbar.Toggle name="Highlight" icon={<div>🖊️</div>} active={editor?.isActive("highlight") ?? false} onClick={() => editor?.chain().focus().toggleHighlight().run()} disabled={!editor?.can().chain().focus().toggleHighlight().run()} /> </FloatingToolbar> );}

To learn more about the different components, read more under Toolbar.

Props

  • editorEditor | nullRequired

    The Tiptap editor.

  • position"top" | "bottom"

    The vertical position of the floating toolbar.

  • offsetnumber

    The vertical offset of the floating toolbar from the selection.

  • childrenReactNode

    The content of the toolbar, overriding the default content. Use the before and after props if you want to keep and extend the default content. Any ReactNode or Toolbar.* components work inside.

  • beforeReactNode

    The content to display at the start of the toolbar. Any ReactNode or Toolbar.* components work inside.

  • afterReactNode

    The content to display at the end of the toolbar. Any ReactNode or Toolbar.* components work inside.

FloatingComposer

Displays a Composer near the current Tiptap selection, allowing you to create threads.

<FloatingComposer editor={editor} />
FloatingComposer

Submitting a comment will attach an annotation thread at the current selection. Should be passed your Tiptap editor, and it’s recommended you set a width value. Display created threads with AnchoredThreads or FloatingThreads.

import {  useLiveblocksExtension,  FloatingComposer,  FloatingThreads,} from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension();
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
return ( <div> <EditorContent editor={editor} /> <FloatingComposer editor={editor} style={{ width: "350px" }} /> <FloatingThreads editor={editor} style={{ width: "350px" }} /> </div> );}

Opening the composer

To open the FloatingComposer, you need to click the “Comment” button in the Toolbar or call the addPendingComment command added by Liveblocks. You can use liveblocksCommentMark to check if the current selection is a comment.

import { Editor } from "@tiptap/react";
function Toolbar({ editor }: { editor: Editor | null }) { if (!editor) { return null; }
return ( <button onClick={() => { editor.chain().focus().addPendingComment().run(); }} data-active={editor.isActive("liveblocksCommentMark")} > 💬 New comment </button> );}

Props

  • metadataThreadMetadata

    The metadata of the thread to create.

  • onComposerSubmitfunction

    The event handler called when the composer is submitted.

  • defaultValueCommentBody

    The composer’s initial value.

  • collapsedboolean

    Whether the composer is collapsed. Setting a value will make the composer controlled.

  • onCollapsedChangefunction

    The event handler called when the collapsed state of the composer changes.

  • defaultCollapsedboolean

    Whether the composer is initially collapsed. Setting a value will make the composer uncontrolled.

  • disabledboolean

    Whether the composer is disabled.

  • autoFocusboolean

    Whether to focus the composer on mount.

  • overridesPartial<GlobalOverrides & ComposerOverrides>

    Override the component’s strings.

FloatingThreads

Displays floating Thread components below text highlights in the editor.

<FloatingThreads editor={editor} threads={threads} />
FloatingThreads

Takes a list of threads retrieved from useThreads and renders them to the page. Each thread is opened by clicking on its corresponding text highlight. Should be passed your Tiptap editor, and it’s recommended you set a width value.

import { useThreads } from "@liveblocks/react/suspense";import {  useLiveblocksExtension,  FloatingComposer,  FloatingThreads,} from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension();
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
const { threads } = useThreads();
return ( <div> <EditorContent editor={editor} /> <FloatingComposer editor={editor} style={{ width: "350px" }} /> <FloatingThreads editor={editor} threads={threads} style={{ width: "350px" }} /> </div> );}

Recommended usage

FloatingThreads and AnchoredThreads have been designed to work together to provide the optimal experience on mobile and desktop. We generally recommend using both components, hiding one on smaller screens, as we are below with Tailwind classes. Most apps also don’t need to display resolved threads, so we can filter those out with a useThreads option.

import { useThreads } from "@liveblocks/react/suspense";import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-tiptap";import { Editor } from "@tiptap/react";
function ThreadOverlay({ editor }: { editor: Editor | null }) { const { threads } = useThreads({ query: { resolved: false } });
return ( <> <FloatingThreads editor={editor} threads={threads} className="w-[350px] block md:hidden" /> <AnchoredThreads editor={editor} threads={threads} className="w-[350px] hidden sm:block" /> </> );}

We can place this component inside ClientSideSuspense to prevent it rendering until threads have loaded.

<div>  <EditorContent editor={editor} />  <FloatingComposer editor={editor} style={{ width: "350px" }} />  <ClientSideSuspense fallback={null}>    <ThreadOverlay editor={editor} />  </ClientSideSuspense></div>

Customization

The FloatingThreads component acts as a wrapper around each individual Thread. You can treat the component like you would a div, using classes, listeners, and more.

<FloatingThreads  editor={editor}  threads={threads}  className="my-floating-thread"/>

To apply styling to each Thread, you can pass a custom Thread property to components and modify this in any way. This is the best way to modify a thread’s width.

import { Thread } from "@liveblocks/react-ui";
<FloatingThreads editor={editor} threads={threads} className="my-floating-thread" components={{ Thread: (props) => ( <Thread {...props} className="border shadow" style={{ width: "300px" }} /> ), }}/>;

You can return any custom ReactNode here, including anything from a simple wrapper around Thread, up to a full custom Thread component built using our Comment primitives.

import { Comment } from "@liveblocks/react-ui/primitives";
<FloatingThreads editor={editor} threads={threads} className="my-floating-thread" components={{ Thread: (props) => ( <div> {props.thread.comments.map((comment) => ( <Comment.Body key={comment.id} body={comment.body} components={/* ... */} /> ))} </div> ), }}/>;

Props

  • threadsThreadData[]Required

    The threads to display.

  • componentsPartial<AnchoredThreadsComponents>

    Override the component’s components.

  • components.Thread(props: ThreadProps) => ReactNode

    Override the Thread component.

AnchoredThreads

Displays a list of Thread components vertically alongside the editor.

<AnchoredThreads editor={editor} threads={threads} />
AnchoredThreads

Takes a list of threads retrieved from useThreads and renders them to the page. Each thread is displayed at the same vertical coordinates as its corresponding text highlight. If multiple highlights are in the same location, each thread is placed in order below the previous thread.

import { useThreads } from "@liveblocks/react/suspense";import {  useLiveblocksExtension,  FloatingComposer,  AnchoredThreads,} from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension();
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
const { threads } = useThreads();
return ( <div> <EditorContent editor={editor} /> <FloatingComposer editor={editor} style={{ width: "350px" }} /> <AnchoredThreads editor={editor} threads={threads} style={{ width: "350px" }} /> </div> );}

Recommended usage

FloatingThreads and AnchoredThreads have been designed to work together to provide the optimal experience on mobile and desktop. We generally recommend using both components, hiding one on smaller screens, as we are below with Tailwind classes. Most apps also don’t need to display resolved threads, so we can filter those out with a useThreads option.

import { useThreads } from "@liveblocks/react/suspense";import { AnchoredThreads, FloatingThreads } from "@liveblocks/react-tiptap";import { Editor } from "@tiptap/react";
function ThreadOverlay({ editor }: { editor: Editor | null }) { const { threads } = useThreads({ query: { resolved: false } });
return ( <> <FloatingThreads editor={editor} threads={threads} className="w-[350px] block md:hidden" /> <AnchoredThreads editor={editor} threads={threads} className="w-[350px] hidden sm:block" /> </> );}

We can place this component inside ClientSideSuspense to prevent it rendering until threads have loaded.

<div>  <EditorContent editor={editor} />  <FloatingComposer editor={editor} style={{ width: "350px" }} />  <ClientSideSuspense fallback={null}>    <ThreadOverlay editor={editor} />  </ClientSideSuspense></div>

Customization

The AnchoredThreads component acts as a wrapper around each Thread. It has no width, so setting this is required, and each thread will take on the width of the wrapper. You can treat the component like you would a div, using classes, listeners, and more.

<AnchoredThreads  editor={editor}  threads={threads}  style={{ width: "350px" }}  className="my-anchored-thread"/>

To apply styling to each Thread, you can pass a custom Thread property to components and modify this in any way.

import { Thread } from "@liveblocks/react-ui";
<AnchoredThreads editor={editor} threads={threads} style={{ width: "350px" }} className="my-anchored-thread" components={{ Thread: (props) => ( <Thread {...props} className="border shadow" style={{ background: "white" }} /> ), }}/>;

You can return any custom ReactNode here, including anything from a simple wrapper around Thread, up to a full custom Thread component built using our Comment primitives.

import { Comment } from "@liveblocks/react-ui/primitives";
<AnchoredThreads editor={editor} threads={threads} style={{ width: "350px" }} className="my-anchored-thread" components={{ Thread: (props) => ( <div> {props.thread.comments.map((comment) => ( <Comment.Body key={comment.id} body={comment.body} components={/* ... */} /> ))} </div> ), }}/>;
Modifying thread floating positions

Using CSS variables you can modify the gap between threads, and the horizontal offset that’s added when a thread is selected.

.lb-tiptap-anchored-threads {  /* Minimum gap between threads */  --lb-tiptap-anchored-threads-gap: 8px;
/* How far the active thread is offset to the left */ --lb-tiptap-anchored-threads-active-thread-offset: 12px;}

Props

  • threadsThreadData[]Required

    The threads to display.

  • componentsPartial<AnchoredThreadsComponents>

    Override the component’s components.

  • components.Thread(props: ThreadProps) => ReactNode

    Override the Thread component.

HistoryVersionPreviewbeta

The HistoryVersionPreview component allows you to display a preview of a specific version of your Tiptap editor’s content. It also contains a button and logic for restoring. It must be used inside the <LiveblocksPlugin> context. To render a list of versions, see VersionHistory.

Usage

import { HistoryVersionPreview } from "@liveblocks/react-tiptap";
function VersionPreview({ selectedVersion, onVersionRestore }) { return ( <HistoryVersionPreview version={selectedVersion} onVersionRestore={onVersionRestore} /> );}

Props

  • versionHistoryVersionRequired

    The version of the editor content to preview.

  • onVersionRestore(version: HistoryVersion) => void

    Callback function called when the user chooses to restore this version.

The HistoryVersionPreview component renders a read-only view of the specified version of the editor content. It also provides a button for users to restore the displayed version.

AiToolbarbeta

Displays a floating AI toolbar near the current Tiptap selection, allowing you to use AI to apply changes to the document or ask questions about it.

<AiToolbar editor={editor} />

Pass your Tiptap editor to use the component, and enable (or customize) the AI option in useLiveblocksExtension.

import {  useLiveblocksExtension,  AiToolbar,  FloatingToolbar,} from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension({ ai: true, });
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
return ( <div> <EditorContent editor={editor} /> <AiToolbar editor={editor} /> <FloatingToolbar editor={editor} /> </div> );}

Opening the toolbar

To open the AiToolbar, you need to call the askAi command added by Liveblocks.

import { Editor } from "@tiptap/react";
function Toolbar({ editor }: { editor: Editor | null }) { if (!editor) { return null; }
return ( <button onClick={() => { editor.chain().focus().askAi().run(); }} > ✨ Ask AI </button> );}

You can also pass a prompt to the askAi command and it will directly request it when opening the toolbar.

<button  onClick={() => {    editor.chain().focus().askAi("Add emojis to the text").run();  }}>  😃 Emojify</button>

Customizing suggestions

By default, the AI toolbar displays a list of suggestions (e.g. “Fix mistakes”, “Explain”, etc). These can be customized via the suggestions prop and the AiToolbar.Suggestion component.

<AiToolbar  editor={editor}  suggestions={    <>      <AiToolbar.SuggestionsLabel>Suggested</AiToolbar.SuggestionsLabel>      <AiToolbar.Suggestion>Fix mistakes</AiToolbar.Suggestion>      <AiToolbar.Suggestion prompt="Add emojis to the text">        Emojify      </AiToolbar.Suggestion>      <AiToolbar.SuggestionsSeparator />      <AiToolbar.Suggestion icon={<Icon.Sparkles />}>        Continue writing      </AiToolbar.Suggestion>    </>  }/>

Doing so will override the default suggestions, instead you can use a function to keep them while adding your own.

<AiToolbar  editor={editor}  suggestions={({ children }) => (    <>      {children}      <AiToolbar.SuggestionsSeparator />      <AiToolbar.SuggestionsLabel>Custom</AiToolbar.SuggestionsLabel>      <AiToolbar.Suggestion>Custom suggestion</AiToolbar.Suggestion>    </>  )}/>

Props

  • editorEditor | nullRequired

    The Tiptap editor.

  • offsetnumber

    The vertical offset of the AI toolbar from the selection.

  • suggestionsReactNode

    The prompt suggestions to display below the AI toolbar.

AiToolbar.Suggestion

A prompt suggestion displayed below the AI toolbar.

import { AiToolbar } from "@liveblocks/react-tiptap";
<AiToolbar editor={editor} suggestions={ <> <AiToolbar.Suggestion>Fix mistakes</AiToolbar.Suggestion> </> }/>;

By default, selecting a suggestion will use its label from the children as the prompt, this can be overridden with the prompt prop. Also optionally takes an icon.

import { AiToolbar } from "@liveblocks/react-tiptap";import { Icon } from "@liveblocks/react-ui";
// "Translate to French" is displayed in the suggestion and used as the prompt<AiToolbar.Suggestion>Translate to French</AiToolbar.Suggestion>
// "Emojify" is displayed in the suggestion but "Add emojis to the text" is used as the prompt<AiToolbar.Suggestion prompt="Add emojis to the text">Emojify</AiToolbar.Suggestion>
// Custom icon<AiToolbar.Suggestion icon={<div>?</div>}>Explain</AiToolbar.Suggestion>
// Using a Liveblocks icon<AiToolbar.Suggestion icon={<Icon.QuestionMark />}>Explain</AiToolbar.Suggestion>
Props
  • childrenReactNode

    The suggestion’s label, used as the prompt if the prompt prop is not set.

  • iconReactNode

    An optional icon displayed before the label.

  • promptstring

    The prompt to use instead of the label.

AiToolbar.SuggestionsLabel

A label to describe a group of prompt suggestions displayed in the AI toolbar.

import { AiToolbar } from "@liveblocks/react-tiptap";
<AiToolbar editor={editor} suggestions={ <> <AiToolbar.SuggestionsLabel>Translation</AiToolbar.SuggestionsLabel> <AiToolbar.Suggestion>Translate in French</AiToolbar.Suggestion> <AiToolbar.Suggestion>Translate in English</AiToolbar.Suggestion> </> }/>;

AiToolbar.SuggestionsSeparator

A separator between groups of prompt suggestions displayed in the AI toolbar.

import { AiToolbar } from "@liveblocks/react-tiptap";
<AiToolbar editor={editor} suggestions={ <> <AiToolbar.Suggestion>Translate in French</AiToolbar.Suggestion> <AiToolbar.Suggestion>Translate in English</AiToolbar.Suggestion> <AiToolbar.SuggestionsSeparator /> <AiToolbar.Suggestion>Custom suggestion</AiToolbar.Suggestion> </> }/>;

Hooks

useLiveblocksExtension

Liveblocks plugin for Tiptap that adds collaboration to your editor. liveblocks should be passed to Tiptap’s useEditor as an extension.

import { useLiveblocksExtension } from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension();
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
return ( <div> <EditorContent editor={editor} /> </div> );}

A number of options can be applied.

const liveblocks = useLiveblocksExtension({  initialContent: "Hello world",  field: "editor-one",
// Other options // ...});
Returns
Arguments
  • options.initialContentContent

    The initial content for the editor, if it’s never been set. Learn more.

  • options.fieldstring

    The name of this text editor’s field. Allows you to use multiple editors on one page, if each has a separate field value. Learn more.

  • options.offlineSupport_experimentalbooleanDefault is false

    Experimental. Enable offline support using IndexedDB. This means the after the first load, documents will be stored locally and load instantly. Learn more.

  • options.commentsbooleanDefault is true

    Enable comments in the editor.

  • options.mentionsbooleanDefault is true

    Enable mentions in the editor.

  • options.aiboolean | AiConfigurationDefault is false

    Enable AI in the editor and optionally customize configuration options.

Setting initial content

Initial content for the editor can be set with initialContent. This content will only be used if the current editor has never been edited by any users, and is ignored otherwise.

import { useLiveblocksExtension } from "@liveblocks/react-tiptap";
function TextEditor() { const liveblocks = useLiveblocksExtension({ initialContent: "<p>Hello world</p>", });
// ...}

Multiple editors

It’s possible to use multiple editors on one page by passing values to the field property. Think of it like an ID for the current editor.

import { useLiveblocksExtension } from "@liveblocks/react-tiptap";
function TextEditor() { const liveblocks = useLiveblocksExtension({ field: "editor-one", });
// ...}

Here’s an example of how multiple editors may be set up.

import { useLiveblocksExtension } from "@liveblocks/react-tiptap";import { useEditor, EditorContent } from "@tiptap/react";
function TextEditors() { return ( <div> <TextEditor field="one" /> <TextEditor field="two" /> </div> );}
function TextEditor({ field }: { field: string }) { const liveblocks = useLiveblocksExtension({ field });
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
return ( <div> <EditorContent editor={editor} /> </div> );}

Offline supportexperimental

It’s possible to enable offline support in your editor with an experimental option. This means that once a document has been opened, it’s saved locally on the browser, and can be shown instantly without a loading screen. As soon as Liveblocks connects, any remote changes will be synchronized, without any load spinner. Enable this by passing a offlineSupport_experimental value.

import { useLiveblocksExtension } from "@liveblocks/react-tiptap";
function TextEditor() { const liveblocks = useLiveblocksExtension({ offlineSupport_experimental: true, });
// ...}

To make sure that your editor loads instantly, you must structure your app carefully to avoid any Liveblocks hooks and ClientSideSuspense components from triggering a loading screen. For example, if you’re displaying threads in your editor with useThreads, you must place this inside a separate component and wrap it in ClientSideSuspense.

"use client";
import { ClientSideSuspense, useThreads } from "@liveblocks/react/suspense";import { useLiveblocksExtension, AnchoredThreads, FloatingComposer,} from "@liveblocks/react-tiptap";import { Editor, EditorContent, useEditor } from "@tiptap/react";
export function TiptapEditor() { const liveblocks = useLiveblocksExtension({ offlineSupport_experimental: true, });
const editor = useEditor({ extensions: [ liveblocks, // ... ], immediatelyRender: false, });
return ( <> <EditorContent editor={editor} /> <FloatingComposer editor={editor} style={{ width: 350 }} /> <ClientSideSuspense fallback={null}> <Threads editor={editor} /> </ClientSideSuspense> </> );}
function Threads({ editor }: { editor: Editor }) { const { threads } = useThreads();
return <AnchoredThreads editor={editor} threads={threads} />;}

Customizing AI componentsbeta

By default, AI components like AiToolbar use the term "AI". This can be customized with the ai.name option in useLiveblocksExtension. This value will be used throughout the AI components: "Ask {name}" in the Toolbar/ FloatingToolbar default buttons, "{name} is thinking…" in the AiToolbar, etc.

import { useLiveblocksExtension } from "@liveblocks/react-tiptap";import { useEditor } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension({ ai: { // "Ask Liveblocks anything…", "Liveblocks is thinking…", etc name: "Liveblocks", }, });
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
// ...}

If you’re after visual customization, AI components like AiToolbar integrate with the rest of Liveblocks' styles, heavily using tokens like --lb-accent for example. Learn more about styling.

Generating AI toolbar responsesbeta

By default, the AiToolbar component sends its requests to Liveblocks to generate its responses. This can be customized via the ai.resolveContextualPrompt option in useLiveblocksExtension. This option accepts an async function which will be called by the AI toolbar whenever a prompt is requested, it will receive the prompt and some context (the document’s and selection’s text, the previous request if it’s a follow-up, etc) and is expected to return the type of response and the text to use.

import { useLiveblocksExtension } from "@liveblocks/react-tiptap";import { useEditor } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension({ ai: { resolveContextualPrompt: async ({ prompt, context, signal }) => { const response = await fetch("/api/contextual-prompt", { method: "POST", body: JSON.stringify({ prompt, context }), signal, });
return response.json(); }, }, });
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
// ...}

useIsEditorReady

Used to check if the editor content has been loaded or not, helpful for displaying a loading skeleton.

import { useIsEditorReady } from "@liveblocks/react-tiptap";
const status = useIsEditorReady();

Here’s how it can be used in the context of your editor.

import { useLiveblocksExtension } from "@liveblocks/react-tiptap";import { useIsEditorReady, useEditor, EditorContent } from "@tiptap/react";
function TextEditor() { const liveblocks = useLiveblocksExtension(); const ready = useIsEditorReady();
const editor = useEditor({ extensions: [ liveblocks, // ... ], });
return ( <div> {!ready ? <div>Loading...</div> : <EditorContent editor={editor} />} </div> );}

Stylesheets

React Tiptap comes with default styles, and these can be imported into the root of your app or directly into a CSS file with @import. Note that you must also install and import a stylesheet from @liveblocks/react-ui to use these styles.

import "@liveblocks/react-ui/styles.css";import "@liveblocks/react-tiptap/styles.css";

Customizing your styles

Adding dark mode and customizing your styles is part of @liveblocks/react-ui, learn how to do this under styling and customization.

We use cookies to collect data to improve your experience on our site. Read our Privacy Policy to learn more.