@liveblocks/react-tiptap
provides you with aReactplugin that adds collaboration to anyTiptap text editor.It also adds realtime cursors, document persistence on the cloud, comments, andmentions. Read ourget started guides tolearn more. Use@liveblocks/node-prosemirror
for server-side editing.
To set up your collaborative Tiptap editor, adduseLiveblocksExtension
to your editor, passing thereturn valueuseEditor
extension array.
import{ useLiveblocksExtension}from"@liveblocks/react-tiptap";import{ useEditor,EditorContent}from"@tiptap/react";
functionTextEditor(){const liveblocks=useLiveblocksExtension();
const editor=useEditor({ extensions:[ liveblocks,// ...],});
return(<div><EditorContenteditor={editor}/></div>);}
Liveblocks Tiptap components should be passededitor
to enable them.
import{ useLiveblocksExtension,FloatingComposer,}from"@liveblocks/react-tiptap";import{ useEditor,EditorContent}from"@tiptap/react";
functionTextEditor(){const liveblocks=useLiveblocksExtension();
const editor=useEditor({ extensions:[ liveblocks,// ...],});
return(<div><EditorContenteditor={editor}/><FloatingComposereditor={editor}style={{ width:"350px"}}/></div>);}
Learn more in ourget started guides.
Displays a toolbar, allowing you to change the styles of selected text. You canadd contentbefore or after, or the toolbar’soptions can becustomized. Afloating toolbar also exists.
<Toolbareditor={editor}/>
Pass your Tiptapeditor
to use the component. By default, one of the toolbarbuttons can create comment threads—to enable this addFloatingComposer
anddisplay threads withAnchoredThreads
orFloatingThreads
.
import{ useLiveblocksExtension,Toolbar,FloatingComposer,FloatingThreads,}from"@liveblocks/react-tiptap";import{ useEditor,EditorContent}from"@tiptap/react";
functionTextEditor(){const liveblocks=useLiveblocksExtension();
const editor=useEditor({ extensions:[ liveblocks,// ...],});
return(<div><Toolbareditor={editor}/><EditorContenteditor={editor}/><FloatingComposereditor={editor}style={{ width:"350px"}}/><FloatingThreadseditor={editor}style={{ width:"350px"}}/></div>);}
You can insert contentbefore
the first button andafter
the last buttonusingbefore
andafter
. Components such asToolbar.Button
andToolbar.Toggle
can be used to create new buttons.
import{Toolbar}from"@liveblocks/react-tiptap";import{Icon}from"@liveblocks/react-ui";
<Toolbareditor={editor}before={<>I'm at the start</>}after={<Toolbar.Buttonname="Help"icon={<Icon.QuestionMark/>}shortcut="CMD-H"onClick={()=>console.log("help")}/>}/>;
For more complex customization, instead readcreating a custom floating 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";
functionCustomToolbar({ editor}:{ editor:Editor|null}){return(<Toolbareditor={editor}> Hello<strong>world</strong></Toolbar>);}
Each part of our default toolbar is available as blocks which can be slottedtogether. This is how the default toolbar is constructed:
import{Toolbar}from"@liveblocks/react-tiptap";import{Editor}from"@tiptap/react";
functionCustomToolbar({ editor}:{ editor:Editor|null}){return(<Toolbareditor={editor}><Toolbar.BlockSelector/><Toolbar.SectionInline/><Toolbar.Separator/><Toolbar.SectionCollaboration/></Toolbar>);}
You can mix these default components with any custom ones of your own. Below theToolbar.SectionHistory
component is added alongside some custom buttonscreated withToolbar.Button
,Toolbar.Toggle
, andIcon
. Thehighlight toggle button requires aTiptap extension.
import{Toolbar}from"@liveblocks/react-tiptap";import{Icon}from"@liveblocks/react-ui";import{Editor}from"@tiptap/react";
functionCustomToolbar({ editor}:{ editor:Editor|null}){return(<Toolbareditor={editor}><Toolbar.SectionHistory/><Toolbar.Separator/><Toolbar.Buttonname="Help"icon={<Icon.QuestionMark/>}shortcut="CMD-H"onClick={()=>console.log("help")}/><Toolbar.Togglename="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.
The Tiptap editor.
The content of the toolbar, overriding the default content. Use thebefore
andafter
props if you want to keep and extend the default content. AnyReactNode
orToolbar.*
components work inside.
The content to display at the start of the toolbar. AnyReactNode
orToolbar.*
components work inside.
The content to display at the end of the toolbar. AnyReactNode
orToolbar.*
components work inside.
A button for triggering actions. Thename
is displayed in a tooltip. Propssuch asonClick
will be passed to the underlyingbutton
element.
import{Toolbar}from"@liveblocks/react-tiptap";
<Toolbareditor={editor}><Toolbar.Buttonname="Question"onClick={(e)=>console.log("Clicked")}/></Toolbar>;
Optionally takes an icon which will visually replace thename
. Also optionallyaccepts a shortcut, which is displayed in the tooltip. Comment key names areconverted to symbols. Here are various examples.
import{Toolbar}from"@liveblocks/react-tiptap";import{Icon}from"@liveblocks/react-ui";
// Button says "Question"<Toolbar.Togglename="Question"onClick={/* ... */}/>
// Tooltip says "Question [⌘+Q]"<Toolbar.Buttonname="Question"shortcut="CMD+Q"onClick={/* ... */}/>
// Custom icon, replaces the name in the button<Toolbar.Buttonname="Question"icon={<div>?</div>}onClick={/* ... */}/>
// Using a Liveblocks icon, replaces the name in the button<Toolbar.Buttonname="Question"icon={<Icon.QuestionMark/>}onClick={/* ... */}/>
// Passing children visually replaces the `name` and `icon`<Toolbar.Buttonname="Question"onClick={/* ... */}> ? Ask a question</Toolbar.Button>
// Props are passed to the inner `button`<Toolbar.Buttonname="Question"style={{ marginLeft:10}}className="custom-button"onMouseOver={()=>console.log("Hovered")}/>
The name of this button displayed in its tooltip. Will also be displayed inthe button if noicon
orchildren
are passed.
An optional icon displayed in this button.
An optional keyboard shortcut displayed in this button’s tooltip. Commonshortcuts such will be replaced by their symbols, for exampleCMD
→⌘
.
A toggle button for values that can be active or inactive. Best used with texteditor commands. Thename
is displayed in a tooltip. Props will be passed tothe underlyingbutton
element.
import{Toolbar}from"@liveblocks/react-tiptap";
<Toolbareditor={editor}><Toolbar.Togglename="Highlight"active={editor?.isActive("highlight")??false}onClick={()=> editor?.chain().focus().toggleHighlight().run()}/></Toolbar>;
The snippet above shows how to use the Toggle with theTiptap highlight extension.The toggle button can also be toggled withuseState
.
import{Toolbar}from"@liveblocks/react-tiptap";import{Editor}from"@tiptap/react";import{ useState}from"react";
functionCustomToggle({ editor}:{ editor:Editor|null}){const[active, setActive]=useState(false);
return(<Toolbar.Togglename="Toggle options"active={active}onClick={()=>setActive(!active)}/>);}
Toolbar.Toggle
optionally takes an icon which will visually replace thename
. 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.Togglename="Highlight"active={/* ... */}onClick={/* ... */}/>
// Tooltip says "Highlight [⌘+H]"<Toolbar.Togglename="Highlight"shortcut="CMD+H"active={/* ... */}onClick={/* ... */}/>
// Custom icon, replaces the name in the button<Toolbar.Togglename="Highlight"icon={<div>🖊</div>}active={/* ... */}onClick={/* ... */}/>
// Using a Liveblocks icon, replaces the name in the button<Toolbar.Togglename="Highlight"icon={<Icon.QuestionMark/>}active={/* ... */}onClick={/* ... */}/>
// Passing children visually replaces the `name` and `icon`<Toolbar.Togglename="Highlight"active={/* ... */}onClick={/* ... */}> 🖊️Highlight</Toolbar.Toggle>
// Props are passed to the inner `button`<Toolbar.Togglename="Highlight"active={/* ... */}onClick={/* ... */}style={{ marginLeft:10}}className="custom-toggle"onMouseOver={()=>console.log("Hovered")}/>
The name of this button displayed in its tooltip. Will also be displayed inthe button if noicon
orchildren
are passed.
Whether the button is toggled.
An optional icon displayed in this button.
An optional keyboard shortcut displayed in this button’s tooltip. Commonshortcuts such will be replaced by their symbols, for exampleCMD
→⌘
.
Adds a dropdown selector for switching between different block types, such astext,heading 1,blockquote. Props will be passed to the innerbutton
element. Can also be placed insideFloatingToolbar
.
import{Toolbar}from"@liveblocks/react-tiptap";
<Toolbareditor={editor}><Toolbar.BlockSelector/></Toolbar>;
If you’d like to change the items shown in the dropdown menu, you can pass acustomitems
array. Below a code block item(Tiptap extension)is added after the default options.
import{Toolbar}from"@liveblocks/react-tiptap";
<Toolbareditor={editor}><Toolbar.BlockSelectoritems={(defaultItems)=>[...defaultItems,{ name:"Code block", icon:<div>❮ ❯</div>,// OptionalisActive:(editor)=> editor.isActive("codeBlock"),setActive:(editor)=> editor.chain().focus().clearNodes().toggleCodeBlock().run(),},]}/></Toolbar>;
By passing alabel
property, you can overwrite the styles of the dropdownitems. The toolbar button will still display thename
, but in the dropdown,thelabel
will be used instead of thename
andicon
. Below, a new item isadded and itslabel
is customized.
import{Toolbar}from"@liveblocks/react-tiptap";
<Toolbareditor={editor}><Toolbar.BlockSelectoritems={(defaultItems)=>[...defaultItems,{ name:"Code block", label:<divclassName="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 representthe 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=(<spanstyle={{ fontSize:18, fontWeight:"bold"}}>Heading 1</span>);}
if(item.name==="Heading 2"){ label=(<spanstyle={{ fontSize:16, fontWeight:"bold"}}>Heading 2</span>);}
if(item.name==="Heading 3"){ label=(<spanstyle={{ fontSize:15, fontWeight:"bold"}}>Heading 3</span>);}
if(item.name==="Blockquote"){ label=(<spanstyle={{ borderLeft:"3px solid gray", paddingLeft:8}}> Blockquote</span>);}
return{...item, label, icon:null,// Hide all icons};})}/>;
The items displayed in this block selector. When provided as an array, thedefault items are overridden. To avoid this, a function can be providedinstead and it will receive the default items.
Adds a visual, and accessible, separator used to separate sections in thetoolbar. Props will be passed to the innerdiv
element. Can also be placedinsideFloatingToolbar
.
import{Toolbar}from"@liveblocks/react-tiptap";
<Toolbareditor={editor}><Toolbar.SectionHistory/></Toolbar>;
Adds a section containingundo andredo buttons. Can also be placed insideFloatingToolbar
.
import{Toolbar}from"@liveblocks/react-tiptap";
<Toolbareditor={editor}><Toolbar.SectionHistory/></Toolbar>;
Adds a section containing inline formatting actions such asbold,italic,underline. Can also be placed insideFloatingToolbar
.
import{Toolbar}from"@liveblocks/react-tiptap";
<Toolbareditor={editor}><Toolbar.SectionInline/></Toolbar>;
Adds a section containing anadd comment button. Can also be placed insideFloatingToolbar
.
import{Toolbar}from"@liveblocks/react-tiptap";
<Toolbareditor={editor}><Toolbar.SectionCollaboration/></Toolbar>;
Displays a floating toolbar near the current Tiptap selection, allowing you tochange styles. You can add contentbefore or after, or the toolbar’soptions can becustomized. Astatic toolbar also exists.
<FloatingToolbareditor={editor}/>
Pass your Tiptapeditor
to use the component. By default, one of the toolbarbuttons can create comment threads—to enable this addFloatingComposer
anddisplay threads withAnchoredThreads
orFloatingThreads
.
import{ useLiveblocksExtension,FloatingToolbar,FloatingComposer,FloatingThreads,}from"@liveblocks/react-tiptap";import{ useEditor,EditorContent}from"@tiptap/react";
functionTextEditor(){const liveblocks=useLiveblocksExtension();
const editor=useEditor({ extensions:[ liveblocks,// ...],});
return(<div><EditorContenteditor={editor}/><FloatingToolbareditor={editor}/><FloatingComposereditor={editor}style={{ width:"350px"}}/><FloatingThreadseditor={editor}style={{ width:"350px"}}/></div>);}
Usingposition
andoffset
you can reposition the toolbar relative to thecurrent selection.position
can be set to"top"
or"bottom"
, andoffset
defines the vertical distance in pixels from the selection.
<FloatingToolbareditor={editor}position="bottom"// Position can be `top` or `bottom`offset={12}// Distance in px from selection/>
You can insert custom contentbefore
the first button andafter
the lastbutton usingbefore
andafter
. Components such asToolbar.Button
andToolbar.Toggle
can be used to create new buttons.
import{Toolbar}from"@liveblocks/react-tiptap";import{Icon}from"@liveblocks/react-ui";
<FloatingToolbareditor={editor}before={<>I'm at the start</>}after={<Toolbar.Buttonname="Help"icon={<Icon.QuestionMark/>}shortcut="CMD-H"onClick={()=>console.log("help")}/>}/>;
For more complex customization, instead readcreating a custom floating toolbar.
By passing elements as children, it’s possible to create a fully custom floatingtoolbar.
import{FloatingToolbar}from"@liveblocks/react-tiptap";import{Editor}from"@tiptap/react";
functionCustomToolbar({ editor}:{ editor:Editor|null}){return(<FloatingToolbareditor={editor}> Hello<strong>world</strong></FloatingToolbar>);}
Each part of our default toolbar is available as blocks which can be slottedtogether. This is how the default floating toolbar is constructed:
import{FloatingToolbar,Toolbar}from"@liveblocks/react-tiptap";import{Editor}from"@tiptap/react";
functionCustomToolbar({ editor}:{ editor:Editor|null}){return(<FloatingToolbareditor={editor}><Toolbar.BlockSelector/><Toolbar.SectionInline/><Toolbar.Separator/><Toolbar.SectionCollaboration/></FloatingToolbar>);}
You can mix these default components with any custom ones of your own. Below theToolbar.SectionHistory
component is added alongside some custom buttonscreated withToolbar.Button
,Toolbar.Toggle
, andIcon
. Thehighlight toggle button requires aTiptap extension.
import{FloatingToolbar,Toolbar}from"@liveblocks/react-tiptap";import{Icon}from"@liveblocks/react-ui";import{Editor}from"@tiptap/react";
functionCustomToolbar({ editor}:{ editor:Editor|null}){return(<FloatingToolbareditor={editor}><Toolbar.SectionHistory/><Toolbar.Separator/><Toolbar.Buttonname="Help"icon={<Icon.QuestionMark/>}shortcut="CMD-H"onClick={()=>console.log("help")}/><Toolbar.Togglename="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 underToolbar
.
The Tiptap editor.
The vertical position of the floating toolbar.
The vertical offset of the floating toolbar from the selection.
The content of the toolbar, overriding the default content. Use thebefore
andafter
props if you want to keep and extend the default content. AnyReactNode
orToolbar.*
components work inside.
The content to display at the start of the toolbar. AnyReactNode
orToolbar.*
components work inside.
The content to display at the end of the toolbar. AnyReactNode
orToolbar.*
components work inside.
Displays aComposer
near the current Tiptap selection, allowing you tocreate threads.
<FloatingComposereditor={editor}/>
Submitting a comment will attach an annotation thread at the current selection.Should be passed your Tiptapeditor
, and it’s recommended you set a widthvalue. Display created threads withAnchoredThreads
orFloatingThreads
.
import{ useLiveblocksExtension,FloatingComposer,FloatingThreads,}from"@liveblocks/react-tiptap";import{ useEditor,EditorContent}from"@tiptap/react";
functionTextEditor(){const liveblocks=useLiveblocksExtension();
const editor=useEditor({ extensions:[ liveblocks,// ...],});
return(<div><EditorContenteditor={editor}/><FloatingComposereditor={editor}style={{ width:"350px"}}/><FloatingThreadseditor={editor}style={{ width:"350px"}}/></div>);}
To open theFloatingComposer
, you need to click the “Comment” button in theToolbar
or call theaddPendingComment
command added by Liveblocks. Youcan useliveblocksCommentMark
to check if the current selection is a comment.
import{Editor}from"@tiptap/react";
functionToolbar({ editor}:{ editor:Editor|null}){if(!editor){returnnull;}
return(<buttononClick={()=>{ editor.chain().focus().addPendingComment().run();}}data-active={editor.isActive("liveblocksCommentMark")}> 💬 New comment</button>);}
The metadata of the thread to create.
The event handler called when the composer is submitted.
The composer’s initial value.
Whether the composer is collapsed. Setting a value will make the composercontrolled.
The event handler called when the collapsed state of the composer changes.
Whether the composer is initially collapsed. Setting a value will make thecomposer uncontrolled.
Whether the composer is disabled.
Whether to focus the composer on mount.
Override the component’s strings.
Displays floatingThread
components below text highlights in the editor.
<FloatingThreadseditor={editor}threads={threads}/>
Takes a list of threads retrieved fromuseThreads
and renders them to thepage. Each thread is opened by clicking on its corresponding text highlight.Should be passed your Tiptapeditor
, and it’s recommended you set a widthvalue.
import{ useThreads}from"@liveblocks/react/suspense";import{ useLiveblocksExtension,FloatingComposer,FloatingThreads,}from"@liveblocks/react-tiptap";import{ useEditor,EditorContent}from"@tiptap/react";
functionTextEditor(){const liveblocks=useLiveblocksExtension();
const editor=useEditor({ extensions:[ liveblocks,// ...],});
const{ threads}=useThreads();
return(<div><EditorContenteditor={editor}/><FloatingComposereditor={editor}style={{ width:"350px"}}/><FloatingThreadseditor={editor}threads={threads}style={{ width:"350px"}}/></div>);}
TheFloatingThreads
component automatically excludes resolved threads fromdisplay. Any resolved threads passed in the threads list will not be shown.
FloatingThreads
andAnchoredThreads
have been designed to worktogether to provide the optimal experience on mobile and desktop. We generallyrecommend using both components, hiding one on smaller screens, as we are belowwith Tailwind classes. Most apps also don’t need to display resolved threads, sowe can filter those out with auseThreads
option.
import{ useThreads}from"@liveblocks/react/suspense";import{AnchoredThreads,FloatingThreads}from"@liveblocks/react-tiptap";import{Editor}from"@tiptap/react";
functionThreadOverlay({ editor}:{ editor:Editor|null}){const{ threads}=useThreads({ query:{ resolved:false}});
return(<><FloatingThreadseditor={editor}threads={threads}className="w-[350px] block md:hidden"/><AnchoredThreadseditor={editor}threads={threads}className="w-[350px] hidden sm:block"/></>);}
We can place this component insideClientSideSuspense
to prevent itrendering until threads have loaded.
<div><EditorContenteditor={editor}/><FloatingComposereditor={editor}style={{ width:"350px"}}/><ClientSideSuspensefallback={null}><ThreadOverlayeditor={editor}/></ClientSideSuspense></div>
TheFloatingThreads
component acts as a wrapper around each individualThread
. You can treat the component like you would adiv
, using classes,listeners, and more.
<FloatingThreadseditor={editor}threads={threads}className="my-floating-thread"/>
To apply styling to eachThread
, you can pass a customThread
propertytocomponents
and modify this in any way. This is the best way to modify athread’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 customReactNode
here, including anything from a simplewrapper aroundThread
, up to a full customThread
component built using ourComment 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.Bodykey={comment.id}body={comment.body}components={/* ... */}/>))}</div>),}}/>;
The threads to display.
Override the component’s components.
Override theThread
component.
Displays a list ofThread
components vertically alongside the editor.
<AnchoredThreadseditor={editor}threads={threads}/>
Takes a list of threads retrieved fromuseThreads
and renders them to thepage. Each thread is displayed at the same vertical coordinates as itscorresponding 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";
functionTextEditor(){const liveblocks=useLiveblocksExtension();
const editor=useEditor({ extensions:[ liveblocks,// ...],});
const{ threads}=useThreads();
return(<div><EditorContenteditor={editor}/><FloatingComposereditor={editor}style={{ width:"350px"}}/><AnchoredThreadseditor={editor}threads={threads}style={{ width:"350px"}}/></div>);}
TheAnchoredThreads
component automatically excludes resolved threads fromdisplay. Any resolved threads passed in the threads list will not be shown.
FloatingThreads
andAnchoredThreads
have been designed to worktogether to provide the optimal experience on mobile and desktop. We generallyrecommend using both components, hiding one on smaller screens, as we are belowwith Tailwind classes. Most apps also don’t need to display resolved threads, sowe can filter those out with auseThreads
option.
import{ useThreads}from"@liveblocks/react/suspense";import{AnchoredThreads,FloatingThreads}from"@liveblocks/react-tiptap";import{Editor}from"@tiptap/react";
functionThreadOverlay({ editor}:{ editor:Editor|null}){const{ threads}=useThreads({ query:{ resolved:false}});
return(<><FloatingThreadseditor={editor}threads={threads}className="w-[350px] block md:hidden"/><AnchoredThreadseditor={editor}threads={threads}className="w-[350px] hidden sm:block"/></>);}
We can place this component insideClientSideSuspense
to prevent itrendering until threads have loaded.
<div><EditorContenteditor={editor}/><FloatingComposereditor={editor}style={{ width:"350px"}}/><ClientSideSuspensefallback={null}><ThreadOverlayeditor={editor}/></ClientSideSuspense></div>
TheAnchoredThreads
component acts as a wrapper around eachThread
. Ithas no width, so setting this is required, and each thread will take on thewidth of the wrapper. You can treat the component like you would adiv
, usingclasses, listeners, and more.
<AnchoredThreadseditor={editor}threads={threads}style={{ width:"350px"}}className="my-anchored-thread"/>
To apply styling to eachThread
, you can pass a customThread
propertytocomponents
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 customReactNode
here, including anything from a simplewrapper aroundThread
, up to a full customThread
component built using ourComment 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.Bodykey={comment.id}body={comment.body}components={/* ... */}/>))}</div>),}}/>;
Using CSS variables you can modify the gap between threads, and the horizontaloffset 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;}
The threads to display.
Override the component’s components.
Override theThread
component.
Version history is currently in private beta. If you would like access to thebeta, pleasecontact us. We’d love tohear from you.
TheHistoryVersionPreview
component allows you to display a preview of aspecific version of your Tiptap editor’s content. It also contains a button andlogic for restoring. It must be used inside the<LiveblocksPlugin>
context. Torender a list of versions, seeVersionHistory
.
import{HistoryVersionPreview}from"@liveblocks/react-tiptap";
functionVersionPreview({ selectedVersion, onVersionRestore}){return(<HistoryVersionPreviewversion={selectedVersion}onVersionRestore={onVersionRestore}/>);}
The version of the editor content to preview.
Callback function called when the user chooses to restore this version.
TheHistoryVersionPreview
component renders a read-only view of the specifiedversion of the editor content. It also provides a button for users to restorethe displayed version.
AI Copilots is currently in private beta. If you would like access to the beta,pleasecontact us. We’d love to hear fromyou.
Displays a floating AI toolbar near the current Tiptap selection, allowing youto use AI to apply changes to the document or ask questions about it.
<AiToolbareditor={editor}/>
Pass your Tiptapeditor
to use the component, and enable (or customize) the AIoption inuseLiveblocksExtension
.
import{ useLiveblocksExtension,AiToolbar,FloatingToolbar,}from"@liveblocks/react-tiptap";import{ useEditor,EditorContent}from"@tiptap/react";
functionTextEditor(){const liveblocks=useLiveblocksExtension({ ai:true,});
const editor=useEditor({ extensions:[ liveblocks,// ...],});
return(<div><EditorContenteditor={editor}/><AiToolbareditor={editor}/><FloatingToolbareditor={editor}/></div>);}
To open theAiToolbar
, you need to call theaskAi
command added by Liveblocks.
import{Editor}from"@tiptap/react";
functionToolbar({ editor}:{ editor:Editor|null}){if(!editor){returnnull;}
return(<buttononClick={()=>{ editor.chain().focus().askAi().run();}}> ✨ Ask AI</button>);}
You can also pass a prompt to theaskAi
command and it will directly requestit when opening the toolbar.
<buttononClick={()=>{ editor.chain().focus().askAi("Add emojis to the text").run();}}> 😃 Emojify</button>
By default, the AI toolbar displays a list of suggestions (e.g. “Fix mistakes”,“Explain”, etc). These can be customized via thesuggestions
prop and theAiToolbar.Suggestion
component.
<AiToolbareditor={editor}suggestions={<><AiToolbar.SuggestionsLabel>Suggested</AiToolbar.SuggestionsLabel><AiToolbar.Suggestion>Fix mistakes</AiToolbar.Suggestion><AiToolbar.Suggestionprompt="Add emojis to the text"> Emojify</AiToolbar.Suggestion><AiToolbar.SuggestionsSeparator/><AiToolbar.Suggestionicon={<Icon.Sparkles/>}> Continue writing</AiToolbar.Suggestion></>}/>
Doing so will override the default suggestions, instead you can use a functionto keep them while adding your own.
<AiToolbareditor={editor}suggestions={({ children})=>(<>{children}<AiToolbar.SuggestionsSeparator/><AiToolbar.SuggestionsLabel>Custom</AiToolbar.SuggestionsLabel><AiToolbar.Suggestion>Custom suggestion</AiToolbar.Suggestion></>)}/>
The Tiptap editor.
The vertical offset of the AI toolbar from the selection.
The prompt suggestions to display below the AI toolbar.
A prompt suggestion displayed below the AI toolbar.
import{AiToolbar}from"@liveblocks/react-tiptap";
<AiToolbareditor={editor}suggestions={<><AiToolbar.Suggestion>Fix mistakes</AiToolbar.Suggestion></>}/>;
By default, selecting a suggestion will use its label from thechildren
as theprompt, this can be overridden with theprompt
prop. Also optionally takes anicon.
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.Suggestionprompt="Add emojis to the text">Emojify</AiToolbar.Suggestion>
// Custom icon<AiToolbar.Suggestionicon={<div>?</div>}>Explain</AiToolbar.Suggestion>
// Using a Liveblocks icon<AiToolbar.Suggestionicon={<Icon.QuestionMark/>}>Explain</AiToolbar.Suggestion>
The suggestion’s label, used as the prompt if theprompt
prop is not set.
An optional icon displayed before the label.
The prompt to use instead of the label.
A label to describe a group of prompt suggestions displayed in the AI toolbar.
import{AiToolbar}from"@liveblocks/react-tiptap";
<AiToolbareditor={editor}suggestions={<><AiToolbar.SuggestionsLabel>Translation</AiToolbar.SuggestionsLabel><AiToolbar.Suggestion>Translate in French</AiToolbar.Suggestion><AiToolbar.Suggestion>Translate in English</AiToolbar.Suggestion></>}/>;
A separator between groups of prompt suggestions displayed in the AI toolbar.
import{AiToolbar}from"@liveblocks/react-tiptap";
<AiToolbareditor={editor}suggestions={<><AiToolbar.Suggestion>Translate in French</AiToolbar.Suggestion><AiToolbar.Suggestion>Translate in English</AiToolbar.Suggestion><AiToolbar.SuggestionsSeparator/><AiToolbar.Suggestion>Custom suggestion</AiToolbar.Suggestion></>}/>;
Liveblocks plugin for Tiptap that adds collaboration to your editor.liveblocks
should be passed to Tiptap’suseEditor
as an extension.
import{ useLiveblocksExtension}from"@liveblocks/react-tiptap";import{ useEditor,EditorContent}from"@tiptap/react";
functionTextEditor(){const liveblocks=useLiveblocksExtension();
const editor=useEditor({ extensions:[ liveblocks,// ...],});
return(<div><EditorContenteditor={editor}/></div>);}
A number of options can be applied.
const liveblocks=useLiveblocksExtension({ initialContent:"Hello world", field:"editor-one",
// Other options// ...});
Returns a LiveblocksTiptapextension.
The initial content for the editor, if it’s never been set.Learnmore.
The name of this text editor’s field. Allows you to use multiple editors onone page, if each has a separate field value.Learnmore.
Experimental. Enable offline support using IndexedDB. This means the afterthe first load, documents will be stored locally and load instantly.Learnmore.
Enable comments in the editor.
Enable mentions in the editor.
Enable AI in the editor and optionally customize configuration options.
Initial content for the editor can be set withinitialContent
. This contentwill only be used if the current editor has never been edited by any users, andis ignored otherwise.
import{ useLiveblocksExtension}from"@liveblocks/react-tiptap";
functionTextEditor(){const liveblocks=useLiveblocksExtension({ initialContent:"<p>Hello world</p>",});
// ...}
It’s possible to use multiple editors on one page by passing values to thefield
property. Think of it like an ID for the current editor.
import{ useLiveblocksExtension}from"@liveblocks/react-tiptap";
functionTextEditor(){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";
functionTextEditors(){return(<div><TextEditorfield="one"/><TextEditorfield="two"/></div>);}
functionTextEditor({ field}:{ field:string}){const liveblocks=useLiveblocksExtension({ field});
const editor=useEditor({ extensions:[ liveblocks,// ...],});
return(<div><EditorContenteditor={editor}/></div>);}
It’s possible to enable offline support in your editor with an experimentaloption. This means that once a document has been opened, it’s saved locally onthe browser, and can be shown instantly without a loading screen. As soon asLiveblocks connects, any remote changes will be synchronized, without any loadspinner. Enable this by passing aofflineSupport_experimental
value.
import{ useLiveblocksExtension}from"@liveblocks/react-tiptap";
functionTextEditor(){const liveblocks=useLiveblocksExtension({ offlineSupport_experimental:true,});
// ...}
To make sure that your editor loads instantly, you must structure your appcarefully to avoid any Liveblocks hooks andClientSideSuspense
componentsfrom triggering a loading screen. For example, if you’re displaying threads inyour editor withuseThreads
, you must place this inside a separatecomponent and wrap it inClientSideSuspense
.
"use client";
import{ClientSideSuspense, useThreads}from"@liveblocks/react/suspense";import{ useLiveblocksExtension,AnchoredThreads,FloatingComposer,}from"@liveblocks/react-tiptap";import{Editor,EditorContent, useEditor}from"@tiptap/react";
exportfunctionTiptapEditor(){const liveblocks=useLiveblocksExtension({ offlineSupport_experimental:true,});
const editor=useEditor({ extensions:[ liveblocks,// ...], immediatelyRender:false,});
return(<><EditorContenteditor={editor}/><FloatingComposereditor={editor}style={{ width:350}}/><ClientSideSuspensefallback={null}><Threadseditor={editor}/></ClientSideSuspense></>);}
functionThreads({ editor}:{ editor:Editor}){const{ threads}=useThreads();
return<AnchoredThreadseditor={editor}threads={threads}/>;}
AI Copilots is currently in private beta. If you would like access to the beta,pleasecontact us. We’d love to hear fromyou.
By default, AI components likeAiToolbar
use the term"AI"
. This can becustomized with theai.name
option inuseLiveblocksExtension
. This valuewill be used throughout the AI components:"Ask {name}"
in theToolbar
/FloatingToolbar
default buttons,"{name} is thinking…"
in theAiToolbar
, etc.
import{ useLiveblocksExtension}from"@liveblocks/react-tiptap";import{ useEditor}from"@tiptap/react";
functionTextEditor(){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 likeAiToolbar
integrate with the rest of Liveblocks' styles, heavily using tokens like--lb-accent
for example. Learn more aboutstyling.
AI Copilots is currently in private beta. If you would like access to the beta,pleasecontact us. We’d love to hear fromyou.
By default, theAiToolbar
component sends its requests to Liveblocks togenerate its responses. This can be customized via theai.resolveContextualPrompt
option inuseLiveblocksExtension
. This optionaccepts an async function which will be called by the AI toolbar whenever aprompt is requested, it will receive the prompt and some context (the document’sand selection’s text, the previous request if it’s a follow-up, etc) and isexpected to return the type of response and the text to use.
import{ useLiveblocksExtension}from"@liveblocks/react-tiptap";import{ useEditor}from"@tiptap/react";
functionTextEditor(){const liveblocks=useLiveblocksExtension({ ai:{resolveContextualPrompt:async({ prompt, context, signal})=>{const response=awaitfetch("/api/contextual-prompt",{ method:"POST", body:JSON.stringify({ prompt, context}), signal,});
return response.json();},},});
const editor=useEditor({ extensions:[ liveblocks,// ...],});
// ...}
Used to check if the editor content has been loaded or not, helpful fordisplaying 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";
functionTextEditor(){const liveblocks=useLiveblocksExtension();const ready=useIsEditorReady();
const editor=useEditor({ extensions:[ liveblocks,// ...],});
return(<div>{!ready?<div>Loading...</div>:<EditorContenteditor={editor}/>}</div>);}
React Tiptap comes with default styles, and these can be imported into the rootof your app or directly into a CSS file with@import
. Note that you must alsoinstall and import a stylesheet from@liveblocks/react-ui
to use thesestyles.
import"@liveblocks/react-ui/styles.css";import"@liveblocks/react-tiptap/styles.css";
Adding dark mode and customizing your styles is part of@liveblocks/react-ui
,learn how to do this understyling and customization.
We use cookies to collect data to improve your experience on our site. Read ourPrivacy Policy to learn more.