Rich text with custom extension and toolbar
Add a custom TipTap mark and a toolbar button that toggles it.
import { Mark, mergeAttributes } from '@tiptap/core'
import { useEffect, useState } from 'react'
import {
DefaultRichTextToolbar,
DefaultRichTextToolbarContent,
TLComponents,
Tldraw,
TldrawUiButton,
preventDefault,
tipTapDefaultExtensions,
useEditor,
useValue,
} from 'tldraw'
import 'tldraw/tldraw.css'
import './RichTextCustomExtension.css'
// There's a guide at the bottom of this file!
interface WavyExtensionOptions {
HTMLAttributes: object
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
wavy: {
setWavy(): ReturnType
toggleWavy(): ReturnType
unsetWavy(): ReturnType
}
}
}
// [1]
const Wavy = Mark.create<WavyExtensionOptions>({
name: 'wavy',
addOptions() {
return {
HTMLAttributes: {},
}
},
parseHTML() {
return [
{
tag: 'span.wavy',
},
]
},
renderHTML({ HTMLAttributes }) {
return [
'span',
mergeAttributes(this.options.HTMLAttributes, { class: 'wavy' }, HTMLAttributes),
0,
]
},
addCommands() {
return {
setWavy:
() =>
({ commands }) =>
commands.setMark(this.name),
toggleWavy:
() =>
({ commands }) =>
commands.toggleMark(this.name),
unsetWavy:
() =>
({ commands }) =>
commands.unsetMark(this.name),
}
},
})
// [2]
const components: TLComponents = {
RichTextToolbar: () => {
const editor = useEditor()
const textEditor = useValue('textEditor', () => editor.getRichTextEditor(), [editor])
// [3]
const [, forceUpdate] = useState(0)
useEffect(() => {
if (!textEditor) return
const handleUpdate = () => forceUpdate((n) => n + 1)
textEditor.on('transaction', handleUpdate)
return () => {
textEditor.off('transaction', handleUpdate)
}
}, [textEditor])
if (!textEditor) return null
return (
<DefaultRichTextToolbar>
<TldrawUiButton
type="icon"
title="Toggle wavy underline"
onClick={() => {
textEditor.chain().focus().toggleWavy().run()
}}
isActive={textEditor.isActive('wavy')}
// [4]
onPointerDown={preventDefault}
>
〰️
</TldrawUiButton>
<DefaultRichTextToolbarContent textEditor={textEditor} />
</DefaultRichTextToolbar>
)
},
}
// [5]
const options = {
text: {
tipTapConfig: {
extensions: [...tipTapDefaultExtensions, Wavy],
},
},
}
export default function RichTextCustomExtensionExample() {
return (
<div className="tldraw__editor">
<Tldraw
persistenceKey="rich-text-custom-extension"
components={components}
options={options}
/>
</div>
)
}
/*
[1]
A TipTap mark extension. It renders as a `<span class="wavy">` (styled in
RichTextCustomExtension.css) and adds `setWavy` / `toggleWavy` / `unsetWavy` commands. The
`declare module '@tiptap/core'` block above tells TypeScript about those commands so
`textEditor.chain().toggleWavy()` type checks.
[2]
Replace the rich text toolbar with our own. `DefaultRichTextToolbar` handles positioning the
toolbar over the text selection; we render our custom button plus the default bold/italic/etc
buttons via `DefaultRichTextToolbarContent`. `editor.getRichTextEditor()` is the TipTap editor
for the text currently being edited, or null when no text is being edited.
[3]
The TipTap editor isn't a tldraw signal, so `isActive` wouldn't otherwise refresh as the
selection moves in and out of marked text. Re-render on every TipTap transaction so the
button's active state stays in sync.
[4]
Preventing the default on pointer down keeps the toolbar button from taking focus away from
the text editor, which would collapse the selection before the command runs.
[5]
Pass the extension list through `options.text.tipTapConfig`. Spread `tipTapDefaultExtensions`
so tldraw's own configuration (StarterKit settings, highlight, keyboard tweaks) stays in place;
passing a bare `StarterKit` instead would drop those.
*/
tldraw's rich text is built on TipTap, so you can extend it with any TipTap extension. This example adds a wavy mark that renders a wavy underline, passes it in through options.text.tipTapConfig.extensions (spread tipTapDefaultExtensions first so tldraw's defaults stay in place), and overrides the RichTextToolbar component to add a button that runs the mark's toggleWavy command.
Try creating a text shape, selecting some text, and clicking the 〰️ button in the toolbar.
Is this page helpful?
Prev
Pin (bindings)Next
Rich text with font options