tiptap-extension-spoiler
v0.1.7
Published
Spoiler block extension for Tiptap
Maintainers
Readme
tiptap-extension-spoiler
Spoiler block extension for Tiptap.
Example
Collapsed spoiler preview:

Expanded spoiler content:

Install
pnpm add tiptap-extension-spoilerEditor usage
Use SpoilerReact when the editor is rendered with @tiptap/react. Import the CSS once near the editor entrypoint.
import { useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import SpoilerReact from 'tiptap-extension-spoiler/react'
import 'tiptap-extension-spoiler/style.css'
const editor = useEditor({
extensions: [
StarterKit,
SpoilerReact.configure({
label: 'Spoiler',
wordLimit: 5,
}),
],
})Commands
The extension adds three commands:
editor.commands.setSpoiler()
editor.commands.unsetSpoiler()
editor.commands.toggleSpoiler()setSpoiler() wraps the selected block content in a spoiler.
unsetSpoiler() removes the active or selected spoiler wrapper and keeps the inner blocks.
toggleSpoiler() removes the spoiler when the selection is inside one, otherwise it wraps the selected blocks.
Toolbar Button
import type { Editor } from '@tiptap/react'
interface SpoilerButtonProps {
readonly editor: Editor | null
}
function SpoilerButton({ editor }: SpoilerButtonProps) {
const isSpoilerActive = editor?.isActive('spoiler') ?? false
const canToggleSpoiler =
(isSpoilerActive
? editor?.can().chain().focus().unsetSpoiler().run()
: editor?.can().chain().focus().setSpoiler().run()) ?? false
const label = isSpoilerActive ? 'Remove spoiler' : 'Spoiler'
function handleClick(): void {
if (!editor) {
return
}
const chain = editor.chain().focus()
if (isSpoilerActive) {
chain.unsetSpoiler().run()
return
}
chain.setSpoiler().run()
}
return (
<button
aria-label={label}
aria-pressed={isSpoilerActive}
disabled={!canToggleSpoiler}
onClick={handleClick}
type="button"
>
Spoiler
</button>
)
}If you do not need separate labels, you can call editor.chain().focus().toggleSpoiler().run() directly.
Static Renderer Usage
Use the base Spoiler extension outside the React editor. getSpoilerPreviewSourceText() keeps text from multiple inner blocks separated before getSpoilerPreviewText() applies the word limit.
import StarterKit from '@tiptap/starter-kit'
import { renderToReactElement } from '@tiptap/static-renderer/pm/react'
import Spoiler, {
getSpoilerPreviewSourceText,
getSpoilerPreviewText,
} from 'tiptap-extension-spoiler'
import 'tiptap-extension-spoiler/style.css'
const content = renderToReactElement({
content: article.content,
extensions: [StarterKit, Spoiler],
options: {
nodeMapping: {
spoiler: ({ children, node }) => {
const previewSourceText = getSpoilerPreviewSourceText(node)
const previewText = getSpoilerPreviewText(previewSourceText, 5)
return <SpoilerReveal previewText={previewText}>{children}</SpoilerReveal>
},
},
},
})Styling
The package exports tiptap-extension-spoiler/style.css. You can override the default values with CSS custom properties:
.tiptap-spoiler {
--spoiler-gap: 10px;
--spoiler-margin: 24px 0;
--spoiler-padding: 16px;
--spoiler-border: 1px solid rgb(255 214 168 / 50%);
--spoiler-radius: 14px;
--spoiler-background: #fff7ed;
--spoiler-text-color: #000000;
--spoiler-control-color: #ff6900;
}