🤌 Rich text editor with real-time collaboration (Tiptap)

Hi @rico.trevisan
Thanks for the update. It would be great if it could erase formatting that doesn’t come from Tiptap as well. On the video below, I copy pasted content from a website. The first paragraph seemed to be cleaned up by the plugin (?) but not the second. And the ‘cleanup’ action doesn’t remove formatting.

And also, you can see, sometimes when I click on an action, the action doesn’t happen and the cursor is sent at the end of the paragraph. It seems random but it happens a lot (but not all the time)…

Any help would be appreciated :folded_hands:

Could you share that example? If you refresh the page, does that HTML renders the same way?

Which version are you using?


I will try to find a way to handle this better inside the plugin.

In the meantime, what I do in this case is:

  • I only use autobinding or updating the content when I know it’s single player.
    • If there’s ever a chance that more people will use this, I move it to a collab server.
  • I hardly ever use autobinding.
  • I only run “save content” on blur.
1 Like

OK thanks for the info. I use the very last version (3.22.5).
I don’t use a collab server at the moment. I have too many users x pieces of content. It would be too expensive for not enough value added. I’m starting to think I’ll put on guardrails on multi-editing.
Yes, the content renders the same after reload. It actually happens the same way on your test app : the pasted content’s formatting is not removed…
As for my problem with the actions pushing the cursor, it still happens event if I remove the ‘when tiptap content is updated’…
Thanks :folded_hands:

I just pushed a version that tries to handle the cursor better but I doubt it is a great solution.

For multiplayer, you could setup your own server with PartyKit on Cloudflare: Deploy to your own Cloudflare account | 🎈 PartyKit Docs

You could have a server without any persistence – meaning that the collab server would always pull the data from Bubble.

Then you have to create an endpoint in your Bubble app that can receive the collab server’s webhooks to then update the doc in your Bubble server.

2 Likes

Hey @rico.trevisan
Thanks for the update. I tried the latest version. The cursor jump seems to happen less often but it still happens sometimes and it doesn’t register the click when it happens (like before). I still don’t understand why it happens on my app and not in your demo…
Partykit + Cloudflare seems interesting :+1:

@rico.trevisan Love the app! It seems like the collaboration is no longer working or set up is a bit unclear. I have tired tiptap cloud and also liveblocks but it looks like the app is not initiating the socket properly. TipTap has update their language around keys and id’s that dont quite match with the app’s fields.

Do you have a template example of collab working?

Thanks!

1 Like

@rico.trevisan I would second this. not sure why but I can not make the collaboration work. the console says “debounce done, updating content“ but I can not see the collaboration UI when I am using two different browses. will send you the link just in case you have to check it out.
appreciate your amazin work on this plugin! thanks

Thanks for the bug reports. I’ll work on this in the next 2 days and get back to you guys.

1 Like

Huge cleanup incoming.

v4.0.0

:high_voltage: Tiptap v3 Upgrade

The entire plugin has been upgraded from Tiptap v2 to Tiptap v3 (@tiptap/core ^3.0.9 and all extensions). This is a major upgrade that brings performance improvements, new APIs, and better extensibility.

  • Tiptap v3 core and all extensions updated to ^3.0.9
  • Hocuspocus provider updated from ^3.2.2 to ^3.4.4
  • BubbleMenu / FloatingMenu now use Floating UI (Tiptap v3) instead of tippy.js — menus are now explicitly hidden before being passed to the extension to prevent flash-of-unstyled-content
  • History → UndoRedo: Replaced the History extension with Tiptap v3’s UndoRedo extension (History is automatically disabled when collaboration is active)
  • TableKit → Individual table extensions: Replaced the bundled TableKit with individual Table, TableRow, TableHeader, and TableCell extensions for finer control
  • CollaborationCursor → CollaborationCaret: Updated to Tiptap v3’s renamed collaboration cursor extension

:building_construction: Architecture Refactor

Major refactoring of the plugin’s internal architecture for better maintainability, reliability, and performance.

Library management overhaul

  • Consolidated window.tiptap namespace: All libraries are now exported as a single organized window.tiptap object instead of scattered window.tiptapXyz globals. The object is structured by category (Core, Basic nodes, Formatting, Block elements, Lists, Advanced, Styling, Interaction, Utilities, Collaboration, Third-party)
  • New extensions exported: UndoRedo, TrailingNode, Focus, Selection now available from @tiptap/extensions

Code reorganization

  • Editor setup moved to initialize.js: The ~760-line editor creation logic that previously lived in update.js has been moved to initialize.js as instance.data.setupEditor(). update.js is now a lean ~110-line file that handles property changes only
  • Stylesheet logic extracted: CSS generation is now a reusable instance.data.applyStylesheet(properties) function callable from both update.js and the collab retry path, eliminating duplication

Script loading

  • Moved from shared.html to headers.html: All scripts (dist.js, lodash, tippy.js styles) moved from the plugin-level shared.html to the element-level headers.html with defer attributes for better loading performance
  • shared.html cleared: No longer loads any scripts at the plugin level

:handshake: Collaboration Improvements

Substantial improvements to the real-time collaboration system across all three providers (Tiptap Cloud, Custom Hocuspocus, Liveblocks).

  • Auth failure retry with exponential backoff: When collaboration authentication fails (e.g., JWT not yet valid on the server), the plugin now automatically retries up to 5 times with exponential backoff delays (1s, 2s, 4s, 8s, 16s). The editor and provider are torn down and re-created on each retry
  • Initial content for empty collab documents: New maybeSetCollabInitialContent() helper handles the race condition between provider sync and editor creation — initial content is set on whichever fires last
  • Collab sync polling fallback: Added an interval-based polling fallback for detecting sync completion, since onSynced may not fire reliably in all providers
  • Graceful JWT waiting: When collaboration is enabled but the JWT token isn’t ready yet, the editor now waits gracefully instead of erroring. A one-time debug warning is shown

New collaboration exposed states

  • collab_status (text) — current connection status (e.g., “connected”, “disconnected”)
  • collab_synced (boolean) — whether the document has synced with the server
  • collab_connected_users (number) — count of connected collaboration users
  • collab_status_changed (event) — fires when the collaboration status changes
  • collab_synced (event) — fires when the document syncs

:locked_with_key: Auth Token Action Rewrite

The server-side JWT action has been completely rewritten and renamed.

  • Renamed: “Generate JWT Key” → “Generate Auth Token”
  • New fields:
    • Document names (comma-separated) — replaces the old Doc ID + Doc ID (list) fields. Now accepts a comma-separated string of allowed document names
    • App ID — included as the aud (audience) claim in the token for security
    • User ID (sub) — included as the sub (subject) claim for traceability and auditing
    • Expiration (seconds) — configurable token lifetime (default: 86400 / 1 day). Previous version had no expiration
  • Plugin secret keys renamed: “Tiptap Cloud JWT secret” → “Tiptap Cloud document server secret”, “Custom collab JWT secret” → “Custom collab document server secret”
  • Improved error handling: Secret keys are now trimmed before use; error messages are more descriptive

:sparkles: New Features

  • New action: Unset All Marks — removes all formatting marks (bold, italic, etc.) from the current selection
  • New exposed state: is_empty (boolean) — reflects whether the editor content is empty, updated on every transaction
  • New exposed state: can_undo (boolean) — whether an undo operation is available
  • New exposed state: can_redo (boolean) — whether a redo operation is available
  • New field: Debug Mode — toggleable debug logging. When enabled, logs detailed [Tiptap] messages to the console. When disabled, no console output is produced (replaces scattered console.log calls)

:bug: Bug Fixes

  • Cursor position preservation: When content is updated programmatically (via autobinding or initialContent change), the cursor position is now saved and restored (clamped to document bounds) instead of jumping to the start
  • Menu elements not found on time: Fixed a race condition where bubble menu and floating menu elements weren’t detected during initialization
  • getSelection fixed: The selectedHTML state now uses generateHTML from the window.tiptap namespace with the editor’s actual extensions, producing correct HTML output
  • CSS loading condition: Fixed a condition where editor styles weren’t applied correctly
  • Debounce timeout cleared on programmatic updates: Prevents stale debounced writes from overwriting programmatic content changes

:wrench: Extensions Refactor

The old error-prone comma-separated list of extension names has been replaced with individual yes/no toggles for each extension. Each toggle is organized into its own labeled section, making extensions easy to discover, toggle on/off, and control dynamically with Bubble expressions.

  • 29 individual yes/no toggles replace the old text field where you had to type exact extension names
  • Organized into sections — Text Formatting, Highlight, Heading (with H1–H6 styling), Blockquote, Structure, Lists, Image, YouTube, Link, Table, Menus, Editor Behavior, Hard Break, Mention, Unique ID, Preserve Attributes
  • Extensions with settings have their related options grouped directly below the toggle (e.g., Table toggle + cell padding, border colors; Link toggle + link colors/CSS; Heading toggle + heading levels + H1–H6 styling)
  • Every toggle has documentation explaining what the extension does, how to use it, and relevant keyboard/markdown shortcuts

:artist_palette: CSS Override Fields Cleanup

All advanced CSS override fields have been renamed, documented, and converted from large textareas to compact single-line inputs.

Before After
h1_advh6_adv H1 CSS overrideH6 CSS override
p_adv Paragraph CSS override
Image properties Image CSS override
YouTube (duplicate name) YouTube CSS override
Blockquote styling Blockquote CSS override
Highlight CSS Highlight CSS override
Bullet lists CSS / Number list CSS Bullet list CSS override / Ordered list CSS override
link_adv, link_unvisited_adv, etc. Link CSS override, Unvisited link CSS override, etc.
CSS for base div // override Base div CSS override

All CSS override fields now use compact inputs (use arbitrary text for more room), and have documentation explaining that they inject raw CSS that overrides the preceding settings (size, color, weight, etc.).

:memo: Preserve Attributes Fields Renamed

Before After
preserve_attributes Preserve HTML attributes
preserved_attributes Preserved attributes (read-only)
preserve_unknown_ta... (truncated) Preserve unknown HTML tags

All three fields now have documentation explaining what they do and how they relate to each other.

:card_index_dividers: Property Panel Reorganization

The entire property panel has been reorganized into a clean hierarchy: General config → Stylesheet → Extensions (with toggles + related settings) → Collaboration → Debug mode.

:broom: Cleanup

  • Proper reset handling: reset.js now cleans up collab retry timers and sync polling intervals instead of just logging
  • All element actions updated with consistent editor-ready guards (returnAndReportErrorIfEditorNotReady)
  • Console logging gated: All console.log calls replaced with the instance.data.debug() helper, controlled by the Debug Mode field
2 Likes

@rico.trevisan amazing work, many thanks for your efforts!
just sth that I still cannot understand (and probably not related to this new version): how can I make the collaboration cursor look more like the one that you have in the first post here rather than the one I have right now (it is just a new line with the name of the editor)? is it sth I have to do in bubble or is it some configuration in the plugin? thanks

It was a bug, fix in…

v4.1.0

:sparkles: New Extensions

  • Trailing Node (on by default) — Automatically inserts an empty paragraph at the end of the document when the last node is a block element (table, image, code block, etc.). Prevents users from getting trapped with no way to continue typing below block-level content.
  • Focus (off by default) — Adds a has-focus CSS class to the node where the cursor is currently positioned. Enables styling the active block — e.g., a subtle background or left border on the focused paragraph. Configurable mode: deepest (default, innermost node only), shallowest (outermost node only), or all (all ancestor nodes).
  • Selection (off by default) — Keeps the text selection visually highlighted when the editor loses focus. Useful when users click toolbar buttons outside the editor — the selection remains visible instead of disappearing. A default blue highlight style (.selection { background: #accef7 }) is provided automatically. Includes a Selection CSS override field to customize the highlight styling.

:handshake: Collaboration

  • New field: Cursor label CSS override — Inject custom CSS for the collaboration cursor label (the floating name tag above each user’s cursor). Customize font-size, padding, opacity, etc.

How the collaboration cursor works

Each collaborator’s cursor renders two elements:

  1. The caret (.collaboration-carets__caret) — a thin vertical line at the cursor position, colored with border-color from the user’s cursor_color.
  2. The name label (.collaboration-carets__label) — a floating tag above the caret showing the user’s name, with background-color set to the user’s cursor_color.

The plugin provides sensible defaults (12px font, 600 weight, rounded corners, positioned above the caret). To customize, use the Cursor label CSS override field. Any CSS you add is injected into the .collaboration-carets__label rule and overrides the defaults.

Default label styles:

.collaboration-carets__label {
    position: absolute;
    top: -1.4em;
    left: -1px;
    font-size: 12px;
    font-style: normal;
    font-weight: 600;
    line-height: normal;
    user-select: none;
    color: #0D0D0D;
    padding: 0.1rem 0.3rem;
    border-radius: 3px 3px 3px 0;
    white-space: nowrap;
    /* background-color is set inline from cursor_color */
}

Example overrides:

  • Larger label: font-size: 14px; padding: 0.2rem 0.5rem;
  • Semi-transparent: opacity: 0.7;
  • White text on colored background: color: #fff;
  • Different shape: border-radius: 8px;

:bug: Bug Fixes

  • Collaboration cursor styles not applied: Fixed CSS class name mismatch from the v3 upgrade — the stylesheet targeted .collaboration-cursor__caret / .collaboration-cursor__label but Tiptap v3’s CollaborationCaret extension renders with .collaboration-carets__caret / .collaboration-carets__label. The floating name label now displays correctly with proper positioning, styling, and background color.
1 Like

Fixing some old pains with my buddy Claude

v4.1.1

:bug: Bug Fixes

  • Auto-binding ignored when collaboration is active: Fixed a critical conflict when both collaboration and auto-binding were enabled simultaneously. The auto-binding system works by writing editor HTML to a Bubble database field and reading changes back via setContent(). However, collaboration uses a Y.js CRDT document as the source of truth, and setContent() replaces the entire document — destroying the Y.js state, losing remote edits, and causing content duplication or cursor jumps. When both were on, every user was continuously writing HTML to the same Bubble field (their own and remote changes), creating race conditions and feedback loops that corrupted the collaborative document. Now, when collaboration is active, auto-binding writes and read-backs are suppressed. The collaborative Y.js document takes priority as the single source of truth. A one-time debugger warning is shown if both are enabled. States (contentHTML, contentText, contentJSON) and the contentUpdated event continue to work normally.

:memo: Documentation

  • “Enable collaboration?” field: Added documentation explaining that auto-binding is automatically ignored while collaboration is active to prevent conflicts.
1 Like

That’s some solid refactoring. Properly fitting code blocks into Bubble’s plugin environment is always a pain.

v4.2.0

:sparkles: New Options on Existing Extensions

  • CodeBlock: Tab indentation — New checkbox to allow the Tab key to indent inside code blocks instead of moving focus. Includes a configurable Tab size (default: 4 spaces).
  • Link: Additional protocols — New text field to specify additional protocols to recognize as valid links (e.g. tel, mailto, ftp). Comma-separated.
  • YouTube: Default width & height — New number fields to set the default pixel dimensions for embedded YouTube videos (defaults: 640×480).
  • Mention: Trigger character — New text field to change the character that triggers the mention popup (default: @). Use # for hashtags, etc.
  • Image: Inline images — New checkbox to make images flow inline with text instead of being block-level elements.
  • Table: Cell min width — New number field to set the minimum column width in pixels when resizing tables (default: 25px).

:artist_palette: New CSS Override Fields

  • Horizontal rule CSS override — Inject custom CSS for <hr> elements.
  • Inline code CSS override — Inject custom CSS for inline <code> elements (does not affect code inside code blocks).
  • Code block CSS override — Inject custom CSS for code block (<pre>) elements.
  • Subscript CSS override — Inject custom CSS for <sub> elements.
  • Superscript CSS override — Inject custom CSS for <sup> elements.
  • Task list CSS override — Inject custom CSS for task list checkboxes.

v4.3.0

:sparkles: New Extensions

  • Details / Accordion — Collapsible <details>/<summary> blocks for FAQs, toggleable sections, and accordion-style content. Includes:

    • Extension toggle: Details / Accordion (checkbox, default off)
    • Persist open state (checkbox, default off) — preserves open/closed state in the document
    • CSS overrides: Details CSS override (details container) and Details summary CSS override (summary element)
    • New action: Toggle Details — wraps/unwraps current selection in a details block
    • New exposed state: selection is details (boolean) — whether cursor is inside a details block
  • Invisible Characters — Shows paragraph marks (¶), spaces (·), and hard breaks for power users who need to see whitespace. Includes:

    • Extension toggle: Invisible Characters (checkbox, default off)
    • Start visible (checkbox, default on)
    • Invisible characters CSS override for character decorations
    • New action: Toggle Invisible Characters — toggles visibility on/off
    • New exposed state: Invisible characters visible (boolean)

:wrench: Bug Fixes

  • Fixed duplicate field names that could cause encoding issues

A few of you have asked for the comments extension. However, that is a paid plugin and I cannot add to this plugin. You have 2 options:

  • fork this plugin (use this repo with Pled)
  • hire me to build you a private plugin (DM me)

FYI: Some of the other paid extensions and the corresponding plan.

Extension Plan
AI Generation Start
AI Toolkit Addon
Comments Start
Compare Snapshots Team
Export (docx/odt/md) Start
Import (docx/odt/md) Start
Pages Team
Server AI Toolkit Addon

I’ve (re)launched a collaboration service (like Tiptap.dev or Liveblocks). There’s a free tier so you can give it a try: https://tiptap.rico.wtf/

It is still in beta so all feedback is welcome.

2 Likes

v4.5.0

:bug: Bug Fix — Table selection

Fixed an issue where selecting text inside a table cell with the mouse could cause the entire table to appear highlighted instead of just the text within the cell. This happened because prosemirror-tables converts accidental table-level selections into a cell selection covering all cells. A new Allow table node selection option lets users control this behavior.

:sparkles: New Table Options

Four new configuration options for the Table extension:

  • Resizable columns (checkbox, default on) — Controls whether table columns can be resized by dragging column borders. Previously hardcoded to true. Disable for simpler tables with equal-width columns and no resize handles.
  • Allow table node selection (checkbox, default off) — Controls what happens when the table itself is selected (e.g. by clicking near its border). When off (default, previous behavior), the selection is converted to a cell selection covering all cells — which can cause the entire table to appear highlighted. When on, the table is selected as a single block, which is less disruptive and easier to dismiss.
  • Last column resizable (checkbox, default on) — Whether the rightmost column of the table can be resized. Disabling this prevents unexpected table expansion when dragging the last column border.
  • Resize handle width (number, default 5) — Width in pixels of the invisible resize handle zone on column borders. A larger value makes resizing easier to trigger; a smaller value reduces accidental resize activation near cell edges.

:sparkles: New Server-Side Action

  • Convert webhook payload to HTML — Converts Hocuspocus / Tiptap Cloud webhook payloads into clean HTML for storage in the database. When collaboration is active, the document lives on the collab server as a Y.js document and webhooks deliver changes as ProseMirror JSON — not HTML. This action bridges that gap.
    • Smart format detection — Accepts three input formats automatically:
      1. Full Hocuspocus webhook body (auto-extracts payload.document)
      2. Document object with named fields (e.g. { "default": { "type": "doc", ... } })
      3. Raw ProseMirror JSON (e.g. { "type": "doc", "content": [...] })
    • Field name parameter (optional, default: "default") — for custom Hocuspocus setups using a different Y.js field name
    • Returns: html (text), error (text), returned_an_error (boolean)
    • Supports all editor node/mark types: headings, bold, italic, underline, strike, code, blockquotes, lists, task lists, tables, images, YouTube embeds, links, highlights, text color, font family, font size, subscript, superscript, details/accordion, mentions, and horizontal rules

v4.6.0

:sparkles: Editor Rebuild on Document ID Change

When the collaborative document ID (collab_doc_id) changes at runtime, the editor now automatically tears down and rebuilds with a new provider connected to the new document. Previously, changing the document ID required a page reload.

  • Automatic teardown & rebuild — When collab_doc_id changes while the editor is active in collaboration mode, the existing provider and editor are cleanly destroyed (connections closed, timers cleared, DOM removed) and a fresh editor is created for the new document.
  • New event: Collaboration document changed — Fires just before the teardown begins, allowing workflows to react to the document switch (e.g., save state, show a loading indicator).
  • Shared teardown function — Introduced a reusable teardownEditor() helper that handles full cleanup: collab sync polling, retry timers, debounce timeouts, provider destruction, editor destruction, DOM removal, and state reset. Used by document ID change detection, collab auth failure retries, and element reset — eliminating duplicated teardown logic.
  • Improved reset cleanupreset.js now uses the shared teardown for complete resource cleanup instead of only clearing timers.

:wrench: Minor Improvements

  • App ID → Doc Server ID — The app_id collaboration field has been renamed to match Tiptap’s current naming: Doc Server ID.
  • Collab status values in inspector — The possible collab_status values (disconnected → connecting → connected → synced) are now documented directly in the plugin property panel for easy reference.