wotz/filament-custom-tiptap-extensions
Adds custom TipTap extensions to Filament rich text editors, letting you register extra editor features such as custom nodes, marks, and toolbar tools. Extends Filament's built-in TipTap integration with configurable, reusable extensions for richer content editing.
Pull the package in and wire it into a single editor first, rather than trying to upgrade every rich-text field at once.
composer require wotz/filament-custom-tiptap-extensions
Most Tiptap extension packages don't need asset publishing because Filament Tiptap ships the JS bridge — check the README for a vendor:publish step anyway, since tool icons/labels sometimes live in a Blade view.
The entry point is the FilamentTiptapEditor field. Register the extension and its matching tool together:
use FilamentTiptapEditor\TiptapEditor;
use Wotz\FilamentCustomTiptapExtensions\Extensions\Underline;
use Wotz\FilamentCustomTiptapExtensions\Tools\UnderlineButton;
TiptapEditor::make('body')
->profile('default')
->extensions([
Underline::make(),
])
->tools([
UnderlineButton::class,
]);
The first real use case is almost always "the client wants one more inline format that core Tiptap doesn't expose." Start with whichever extension corresponds to the smallest missing affordance (underline, highlight, sub/superscript, etc.), confirm the button appears, type some content, save the record, and inspect the stored HTML. That single loop validates the whole install before you scale it out.
If the extension only exists client-side in your head, read the package source under src/Extensions and src/Tools first — the class names and make() signature are the contract, and they're short enough to skim in a minute.
Register once, globally, via a shared form schema. Rather than repeating ->extensions([...])->tools([...]) on every resource, extract a helper:
// app/Filament/Support/RichText.php
final class RichText
{
public static function make(string $name = 'body'): TiptapEditor
{
return TiptapEditor::make($name)
->profile('default')
->extensions([
Underline::make(),
Highlight::make(),
// ...
])
->tools([
UnderlineButton::class,
HighlightButton::class,
]);
}
}
Every resource then calls RichText::make() and inherits the same schema, so a later upgrade is one file, not thirty.
Pair extension + tool deliberately. The extension is what Tiptap knows about the node/mark; the tool is the toolbar button. Forgetting either side is the #1 reason "it installed but nothing shows up." Treat them as a tuple and keep them next to each other in your helper.
Register custom tools on top of a profile, not instead of one. Profiles (default, minimal, all) already configure a baseline of extensions. Add yours as an overlay so you don't accidentally strip core behaviour:
TiptapEditor::make('body')
->profile('minimal') // baseline
->extensions([...]) // additive
->tools([...]); // additive
Render output with RichContentRenderer. Whatever HTML the extra marks produce, render it through the same renderer so the JSON/HTML round-trip stays consistent and any block-level extensions (details, anchors, etc.) get the markup they expect:
{!! (new RichContentRenderer($post->body))->render() !!}
Conditional tools per role. If a content editor shouldn't get the fancy blocks, gate the tools array:
->tools(fn () => auth()->user()->isAdmin() ? $adminTools : $basicTools)
This is the cheapest way to keep non-technical editors away from extensions whose output your frontend CSS hasn't caught up with.
Test at the boundary you own. You're not testing Tiptap itself — you're testing that your form accepts the value and that your renderer emits it. A feature test that livewire()->fillForm([...])->call('save') and asserts on the persisted HTML plus one Blade render snapshot catches the realistic failure modes (tool not registered, sanitizer stripping the tag, renderer dropping an attribute).
filament/tiptap-editor pins its own Tiptap major. A minor bump on either side can produce "extension registered but silently ignored" in the browser with no PHP error. After every composer update, open one editor and click every button on your custom tools.filament/tiptap-editor already registers an extension with the same name (underline and highlight are the classic offenders), you'll end up with two marks fighting for the same tag. Prefer the package's extension only where core doesn't cover it, and don't stack both.body column already contains HTML from a plain RichEditor, switching to TiptapEditor gives you JSON by default. Decide intentionally — ->output(Output::HTML) keeps the column readable and your frontend unchanged; JSON gives you richer structure but forces every consumer through the renderer.strip_tags or an HTML purifier anywhere in the pipeline, a new extension's output (details, marks with custom attributes) quietly disappears between save and publish. Extend the allowlist at the same time you add the extension..tiptap-highlight, details > summary, etc.) alongside the deploy, or editors will report the feature "doesn't work" when it's just unstyled.How can I help you explore Laravel packages today?