A drop-in TipTap WYSIWYG editor for Strapi v5.
Rich text, tables, images, and more, configured in minutes.
- Table of Contents
- Rich text editing powered by TipTap - a modern, extensible WYSIWYG editor built on ProseMirror
- Headings (H1–H6), bold, italic, underline, strikethrough
- Ordered & unordered lists, task lists
- Links, tables
- Images from Strapi Media Library with alt text editing and alignment
- Code blocks with syntax highlighting
- Blockquotes, horizontal rules
- Full keyboard shortcut support
- Seamless integration with Strapi's content management system
This plugin is compatible with Strapi v5.x.x and has been tested on Strapi v5.34.0. We expect it should also work on older version of Strapi V5.
| Plugin version | Strapi Version | Full Support |
|---|---|---|
| 1.0.0 | 5.34.0 | ✅ |
# NPM
npm i @senacor/strapi-plugin-tiptap-editor
# Yarn
yarn add @senacor/strapi-plugin-tiptap-editor
yarn build
yarn startThe plugin uses a preset system. A preset is a named configuration that defines which editor tools are available. You define presets in your Strapi plugin config file, then assign them to individual fields via the Content-Type Builder.
Create or update the plugin configuration file at config/plugins.ts (or config/plugins.js):
// config/plugins.ts
export default () => ({
'tiptap-editor': {
config: {
presets: {
// Preset name -> feature configuration
minimal: {
bold: true,
italic: true,
link: true,
},
},
},
},
});Only features explicitly set to true (or an options object) will appear in the toolbar. Any feature not listed, or set to false, will be hidden.
- In the Strapi admin, open the Content-Type Builder.
- Add or edit a field and choose the Rich Text (Tiptap) custom field type.
- In the Advanced Settings tab, select a preset from the Editor Preset dropdown.
- Save the content type.
The editor for that field will now show only the tools defined in the selected preset.
You can define as many presets as you need. Different fields (even within the same content type) can use different presets:
// config/plugins.ts
export default () => ({
'tiptap-editor': {
config: {
presets: {
// A minimal preset for short-form content like titles or captions
minimal: {
bold: true,
italic: true,
underline: true,
},
// A standard preset for blog posts and articles
standard: {
bold: true,
italic: true,
underline: true,
strike: true,
heading: true,
bulletList: true,
orderedList: true,
blockquote: true,
link: true,
},
// A full preset with every feature enabled
full: {
bold: true,
italic: true,
underline: true,
strike: true,
code: true,
codeBlock: true,
heading: true,
blockquote: true,
bulletList: true,
orderedList: true,
link: true,
table: true,
textAlign: true,
superscript: true,
subscript: true,
mediaLibrary: true,
},
},
},
},
});| Key | Description | Toolbar | Keyboard Shortcut |
|---|---|---|---|
bold |
Bold text | B button | Ctrl/Cmd + B |
italic |
Italic text | I button | Ctrl/Cmd + I |
underline |
Underlined text | U button | Ctrl/Cmd + U |
strike |
Strikethrough text | Ctrl/Cmd + Shift + S |
|
code |
Inline code | <> button |
Ctrl/Cmd + E |
superscript |
Superscript text | x^2 button | Ctrl/Cmd + . |
subscript |
Subscript text | x_2 button | Ctrl/Cmd + , |
Usage: Set to true to enable with defaults.
{
bold: true,
italic: true,
underline: true,
strike: true,
code: true,
superscript: true,
subscript: true,
}| Key | Description | Toolbar |
|---|---|---|
blockquote |
Block quotes | Quote button |
codeBlock |
Fenced code blocks | (via keyboard or markdown input) |
bulletList |
Unordered lists | Bullet list button |
orderedList |
Numbered lists | Numbered list button |
Usage: Set to true to enable.
{
blockquote: true,
codeBlock: true,
bulletList: true,
orderedList: true,
}| Key | Description | Toolbar |
|---|---|---|
heading |
Heading levels (h1-h6) | Style dropdown; optional SEO tag and jump-link controls |
The heading extension provides a style selector for visual heading levels. The SEO tag selector is optional and lets content editors set the semantic HTML tag independently from the visual heading level. It is hidden by default and must be enabled explicitly with seoTag: true in the heading options.
Simple usage — enables all heading levels (h1-h6):
{
heading: true,
}This shows the heading style selector and hides the SEO tag and jump-link controls.
Custom levels — restrict which heading levels are available:
{
// Only allow h1, h2, and h3 in the style dropdown
heading: {
levels: [1, 2, 3],
seoTag: true,
jumpLinks: true,
},
}The levels array accepts values from 1 to 6. Set seoTag: true to show the SEO tag dropdown. Omit it or set it to false to hide that control. When shown, the SEO tag dropdown offers all six levels (h1-h6) regardless of the visual heading levels.
Existing presets that relied on the previously always-visible SEO tag selector must add seoTag: true inside their heading options to keep it available. Hiding the selector does not remove SEO tags already stored in heading content.
Set jumpLinks: true inside the heading options to enable heading anchor IDs and suggestions in the link dialog. Omit it or set it to false to hide those controls. Existing IDs remain in stored content when the controls are hidden, and manually entered #id links still work.
When enabled, select a heading and use the # toolbar button to add, edit, or remove its ID in a dialog. The ID must be unique within that editor field. Surrounding whitespace is trimmed, while spaces within the ID and a leading # are rejected. A heading with ID details can be linked with #details. Copying a heading within the same document clears the copied ID when it would be a duplicate.
The ID is stored as an optional id attribute on the heading node in Tiptap JSON. When rendering stored JSON on a frontend, extend the Heading extension to recognize that attribute so the output includes id="details":
import Heading from '@tiptap/extension-heading';
const HeadingWithAnchor = Heading.extend({
addAttributes() {
return {
...this.parent?.(),
id: { default: null },
};
},
});Use HeadingWithAnchor in the frontend renderer's extension list in place of the default Heading extension. Existing content without IDs remains valid.
| Key | Description | Toolbar |
|---|---|---|
link |
Hyperlinks | Link button + link dialog |
Links open a dialog where editors can enter a URL. By default, links do not open on click in the editor (to allow editing).
When heading.jumpLinks is enabled and headings in the current editor field have anchor IDs, the link dialog suggests them by heading text and ID. Choosing one fills the URL with #<id>. Manual URLs, link editing, and link removal remain available. Renaming or removing an anchor does not rewrite existing links.
Simple usage:
{
link: true,
}With options:
{
link: {
openOnClick: true, // Open links on click in the editor (default: false)
HTMLAttributes: {
rel: 'noopener noreferrer',
target: '_blank',
},
},
}| Key | Description | Toolbar |
|---|---|---|
table |
Insertable and editable tables | Table button + column/row controls |
Enables table insertion with controls for adding/removing columns and rows. Tables are resizable by default.
{
table: true,
}| Key | Description | Toolbar |
|---|---|---|
textAlign |
Text alignment controls | Left, Center, Right, Justify buttons |
Enables all four alignment buttons (left, center, right, justify).
{
textAlign: true,
}| Key | Description | Toolbar |
|---|---|---|
textColor |
Change the color of selected text | Font color picker |
highlightColor |
Apply a background highlight | Highlight color picker |
Both features use a color picker popover that displays the colors defined in the theme configuration. If no colors are configured, the buttons will not appear.
{
textColor: true,
highlightColor: true,
}| Key | Description | Toolbar |
|---|---|---|
mediaLibrary |
Images from Strapi Media Library | Image button + alt text popover + alignment + resize handle |
Enables image insertion from the Strapi Media Library. When enabled, the toolbar shows an image button that opens the Media Library picker. After selecting an image:
- The image appears in the editor at its natural size (constrained to editor width)
- Alt text is prefilled from the asset's
alternativeTextmetadata - Clicking a selected image opens a popover with:
- Three alignment buttons (left, center, right)
- Width and Height inputs (in pixels) for precise sizing
- A reset button to restore the original dimensions
- Alt text input and a delete button
- A resize handle (blue dot) appears at the bottom-right corner on hover — drag it to resize the image
The image stores the URL (src), Strapi asset ID (data-asset-id), alignment (data-align), and dimensions (width, height) in the Tiptap JSON output.
Content safety: If you remove mediaLibrary from a preset, existing images in content are preserved and rendered read-only — they are never silently deleted.
{
mediaLibrary: true,
}Resize is configured through the resize key inside mediaLibrary. The options match the standard @tiptap/extension-image resize configuration. When resize is omitted or set to false, the resize handle and dimension controls are hidden.
{
mediaLibrary: {
resize: {
enabled: true,
alwaysPreserveAspectRatio: true,
minWidth: 50,
minHeight: 50,
},
},
}| Option | Default | Description |
|---|---|---|
resize |
none | Set to false or omit to disable resize entirely |
resize.enabled |
true |
Enable or disable resize when the object is present |
resize.alwaysPreserveAspectRatio |
true |
Lock aspect ratio when resizing or editing dimensions |
resize.minWidth |
50 |
Minimum allowed width in pixels |
resize.minHeight |
50 |
Minimum allowed height in pixels |
The plugin stores content as Tiptap/ProseMirror JSON. The width, height, src, alt, and title attributes are standard and will render automatically with @tiptap/extension-image. However, the custom data-align and data-asset-id attributes require extending the Image extension on your frontend:
import { generateHTML } from '@tiptap/html';
import StarterKit from '@tiptap/starter-kit';
import Image from '@tiptap/extension-image';
// Extend with the custom attributes used by this plugin
const StrapiImage = Image.extend({
addAttributes() {
return {
...this.parent?.(),
'data-align': { default: null },
'data-asset-id': { default: null },
};
},
});
// Convert the JSON from the Strapi API to HTML
const html = generateHTML(apiResponse.content, [
StarterKit,
StrapiImage,
// ...other extensions you use
]);Then add CSS for alignment on your frontend:
img[data-align="center"] {
display: block;
margin-left: auto;
margin-right: auto;
}
img[data-align="right"] {
display: block;
margin-left: auto;
margin-right: 0;
}The theme key in the plugin config lets you define colors for the color pickers and inject custom CSS into the editor.
Define a colors array to populate the text color and highlight color pickers. Each entry needs a label (shown as a tooltip) and a color value (hex, rgb, rgba, hsl, hsla, or CSS variable).
// config/plugins.ts
export default () => ({
'tiptap-editor': {
config: {
theme: {
colors: [
{ label: 'Black', color: '#000000' },
{ label: 'Dark gray', color: '#4A4A4A' },
{ label: 'Red', color: '#E53E3E' },
{ label: 'Orange', color: '#DD6B20' },
{ label: 'Blue', color: '#3182CE' },
{ label: 'Green', color: '#38A169' },
{ label: 'Brand primary', color: 'var(--color-primary)' },
],
},
presets: {
blog: {
bold: true,
italic: true,
textColor: true,
highlightColor: true,
},
},
},
},
});You can inject custom CSS to style the editor content area. There are two options — use one or the other, not both.
Option 1: css — Inline CSS content (recommended for monorepos and production deployments)
Read the file at Strapi startup so the CSS is captured as a string. This works reliably across all environments (local dev, Docker, Azure Container Apps, etc.) because the file is resolved in your app's Node process at boot time.
// config/plugins.ts
import { readFileSync } from 'fs';
export default () => ({
'tiptap-editor': {
config: {
theme: {
css: readFileSync(require.resolve('@repo/design-system/strapi-styles.css'), 'utf-8'),
},
},
},
});Option 2: stylesheet — A URL the browser can fetch directly
Use this when the stylesheet is hosted at a known URL (CDN, public path, etc.).
// config/plugins.ts
export default () => ({
'tiptap-editor': {
config: {
theme: {
stylesheet: 'https://cdn.example.com/editor-styles.css',
},
},
},
});Browser spellchecking is disabled by default. Enable it per editor preset with spellcheck: true:
export default () => ({
'tiptap-editor': {
config: {
presets: {
article: {
bold: true,
spellcheck: true,
},
},
},
},
});When enabled, the editor requests the browser's native spellchecking behavior. For localized Strapi content, the active article locale is used as the editor language (for example de-DE or en-GB). If no valid article locale is available, the browser uses its normal language fallback. Browser support and browser-level spellchecking settings remain authoritative.
Set spellcheck: false, omit the option, or use a preset without it to keep spellchecking disabled. The setting only affects the editor at runtime and does not add metadata to the stored TipTap JSON.
Each feature key in a preset accepts one of these values:
| Value | Meaning |
|---|---|
true |
Feature enabled with default options |
false |
Feature explicitly disabled |
| (key omitted) | Feature disabled (absent keys are treated as disabled) |
{ ... } |
Feature enabled with custom options (merged with defaults) |
Here is a single preset with every available feature enabled and annotated:
// config/plugins.ts
export default () => ({
'tiptap-editor': {
config: {
presets: {
everything: {
// Inline formatting
bold: true,
italic: true,
underline: true,
strike: true,
code: true,
superscript: true,
subscript: true,
// Block elements
blockquote: true,
codeBlock: true,
bulletList: true,
orderedList: true,
// Headings — all levels, with optional SEO tags and jump links
heading: {
levels: [1, 2, 3, 4, 5, 6],
seoTag: true,
jumpLinks: true,
},
// Links — custom HTML attributes
link: {
HTMLAttributes: {
rel: 'noopener noreferrer',
},
},
// Tables
table: true,
// Text alignment (left, center, right, justify)
textAlign: true,
// Text and highlight colors (requires theme.colors)
textColor: true,
highlightColor: true,
// Browser-native spellchecking using the active article locale
spellcheck: true,
// Images from Strapi Media Library with resize enabled
mediaLibrary: {
resize: { enabled: true },
},
},
},
},
},
});The plugin validates your configuration at startup. If a preset contains an invalid feature key, Strapi will throw an error with a message listing the invalid keys and all allowed keys. This prevents typos from silently disabling features.
// This will throw an error at startup:
{
presets: {
blog: {
bold: true,
boldd: true, // Typo! Not a valid feature key
},
},
}Fork of notum-cz/strapi-plugin-tiptap-editor.