Introduction
The rich editor allows you to edit and preview HTML content, as well as upload images. It uses TipTap as the underlying editor.Storing content as JSON
By default, the rich editor stores content as HTML. If you would like to store the content as JSON instead, you can use thejson() method:
array cast to the model property:
Customizing the toolbar buttons
You may set the toolbar buttons for the editor using thetoolbarButtons() method. The options shown here are the defaults:
h1- Applies the “h1” tag to the text.h4- Applies the “h4” tag to the text.h5- Applies the “h5” tag to the text.h6- Applies the “h6” tag to the text.alignJustify- Justifies the text.clearFormatting- Clears all formatting from the selected text.details- Inserts a<details>tag, which allows users to create collapsible sections in their content.grid- Inserts a grid layout into the editor, allowing users to create responsive columns of content.gridDelete- Deletes the current grid layout.highlight- Highlights the selected text with a<mark>tag around it.horizontalRule- Inserts a horizontal rule.lead- Applies aleadclass around the text, which is typically used for the first paragraph of an article.paragraph- Sets the current block to a paragraph, removing any heading formatting.small- Applies the<small>tag to the text, which is typically used for small print or disclaimers.code- Format the selected text as inline code.textColor- Changes the text color of the selected text.table- Creates a table in the editor with a default layout of 3 columns and 2 rows, with the first row configured as a header row.tableAddColumnBefore- Adds a new column before the current column.tableAddColumnAfter- Adds a new column after the current column.tableDeleteColumn- Deletes the current column.tableAddRowBefore- Adds a new row above the current row.tableAddRowAfter- Adds a new row below the current row.tableDeleteRow- Deletes the current row.tableMergeCells- Merges the selected cells into one cell.tableSplitCell- Splits the selected cell into multiple cells.tableToggleHeaderRow- Toggles the header row of the table.tableToggleHeaderCell- Toggles the header cell of the table.tableDelete- Deletes the table.
Customizing floating toolbars
If your toolbar is too full, you can use a floating toolbar to show certain tools in a toolbar below the cursor, only when the user is inside a specific node type. This allows you to keep the main toolbar clean while still providing access to additional tools when needed. You can customize the floating toolbars that appear when your cursor is placed inside a specific node by using thefloatingToolbars() method.
In the example below, a floating toolbar appears when the cursor is inside a paragraph node. It shows bold, italic, and similar buttons. When the cursor is in a heading node, it displays heading-related buttons, and when inside a table, it shows table-specific controls.
Grouping toolbar buttons into dropdowns
You may group related toolbar buttons into a dropdown menu usingToolbarButtonGroup. The first argument is a label used for the dropdown’s tooltip and accessibility, and the second argument is an array of button names to include in the dropdown:
icon() method. When a custom icon is set, the trigger icon remains static and does not change based on the active button:
Using textual dropdown toolbar buttons
By default, dropdown toolbar buttons display icons only. If you’d like to show text labels alongside icons in the dropdown items, you can use thetextualButtons() method on a ToolbarButtonGroup:
Paragraph dropdown items display their icon alongside a text label (e.g., “Paragraph”, “Heading 1”). The Alignment dropdown remains icon-only.
Customizing text colors
The rich editor includes a text color tool for styling inline text. By default, it uses the Tailwind CSS color palette. In light mode, the 600 shades are applied to text, and in dark mode, the 400 shades are used. You can customize which colors are available in the picker using thetextColors() method:
TextColor object to define the color:
TextColor::getDefaults() array:
TextColor object, the key of the array becomes the stored data-color attribute on the <span> tag, allowing you to reference the color in your CSS if needed. When you use the color as the array values, the actual color value (e.g., a HEX string) is stored as the data-color attribute.
You can also pass textColors() to the content renderer and rich content attribute so that server-side rendering matches your editor configuration.
You can also allow users to pick custom colors that aren’t in the predefined list by using the customTextColors() method:
customTextColors() on the content renderer, as it will automatically render any custom colors that are used in the content.
Rendering rich content
If you’re storing content as JSON instead of HTML, or your content requires processing to inject private image URLs or similar, you’ll need to use theRichContentRenderer tool in Filament to output HTML:
toHtml() method returns a string. If you would like to output HTML in a Blade view without escaping the HTML, you can echo the RichContentRender object without calling toHtml():
Styling the rendered content
The rich editor HTML uses a combination of HTML elements, CSS classes, and inline styles to style the content, depending on the features used in the editor. If you render the content in a Filament table column or infolist entry withprose(), Filament will automatically apply the necessary styles for you. If you are outputting the content in your own Blade view, you may need to add some additional styles to ensure that the content is styled correctly.
One way of styling the content is to use Tailwind CSS Typography. This plugin provides a set of pre-defined styles for common HTML elements, such as headings, paragraphs, lists, and tables. You can apply these styles to a container element using the prose class:
fi-prose CSS class that adds these additional styles. Any app that loads Filament’s vendor/filament/support/resources/css/index.css CSS will have access to this class. The styling is different to the prose class, but fits with Filament’s design system better:
Security
By default, the editor outputs raw HTML, and sends it to the backend. Attackers are able to intercept the value of the component and send a different raw HTML string to the backend. As such, it is important that when outputting the HTML from a rich editor, it is sanitized; otherwise your site may be exposed to Cross-Site Scripting (XSS) vulnerabilities. When Filament outputs raw HTML from the database in components such asTextColumn and TextEntry, it sanitizes it to remove any dangerous JavaScript. However, if you are outputting the HTML from a rich editor in your own Blade view, this is your responsibility. One option is to use Filament’s sanitizeHtml() helper to do this, which is the same tool we use to sanitize HTML in the components mentioned above:
Filament’s built-in HTML sanitizer permits inline
style attributes in order to support rich text formatting features such as font colors, text highlighting, and image sizing. This means that CSS properties like background: url(...) or position: fixed will not be stripped from sanitized HTML. If your content comes from untrusted users, you should consider restricting the default configuration. See the security documentation for details on how to customize the sanitizer.Uploading images to the editor
By default, uploaded images are stored publicly on your storage disk, so that the rich content stored in the database can be output easily anywhere. You may customize how images are uploaded using configuration methods:Using private images in the editor
Using private images in the editor adds a layer of complexity to the process, since private images cannot be accessed directly via a permanent URL. Each time the editor is loaded or its content is rendered, temporary URLs need to be generated for each image, which are never stored in the database. Instead, Filament adds adata-id attribute to the image tags, which contains an identifier for the image in the storage disk, so that a temporary URL can be generated on demand.
When rendering the content using private images, ensure that you are using the RichContentRenderer tool in Filament to output HTML:
Securing file attachment IDs
Thedata-id attribute on an image node is an identifier for a file on the configured disk. When the content is rendered, Filament generates a URL for it — a signed temporary URL if the visibility is private. Like any other Livewire form field value, the content and its data-id attributes are controlled by the client: a request can be intercepted to change a data-id to any other identifier on the same disk. If the disk also stores files belonging to other users or records, an attacker could otherwise cause the rendered content to reference (and serve a signed URL for) someone else’s file.
Filament allows this by default because legitimate features depend on it — for example, an action that inserts an image from a pre-existing library, or a “copy from another record” button. If none of your editors rely on such a flow, call preventFileAttachmentPathTampering() on the field to enable a built-in check:
$record->getOriginal() for the attribute matching the field name) and allows only the data-id values already present. Any other existing data-id causes the field to fail validation, so the record is never saved with a tampered value. Newly uploaded images always pass through.
The default file attachment provider performs no per-record scoping — any data-id that resolves to a file on the configured disk is accepted unless you enable preventFileAttachmentPathTampering() (or isolate uploads at the disk/directory level). If instead you are using the spatie/laravel-medialibrary plugin as the file attachment provider, this protection is already implicit — it looks up each data-id against the record’s own media collection via $media->has($file), so a data-id for another record’s media is rejected automatically.
To apply this check to every RichEditor in your application without repeating it on each field, call configureUsing() in a service provider’s boot() method:
preventFileAttachmentPathTampering(false).
Allowing additional data-id values with a callback
If your application legitimately references an identifier that is not on the record — for example, a “copy from another record” action — pass the allowFilePathUsing argument to approve it. Approved identifiers bypass the validation error:
validationMessages() using the tampered key:
Validating uploaded images
You may use thefileAttachmentsAcceptedFileTypes() method to control a list of accepted mime types for uploaded images. By default, image/png, image/jpeg, image/gif, and image/webp are accepted:
fileAttachmentsMaxSize() method to control the maximum file size for uploaded images. The size is specified in kilobytes. By default, the maximum size is 12288 KB (12 MB):
Allowing users to resize images
By default, images in the editor cannot be resized by the user. You may enable image resizing using theresizableImages() method:
Using custom blocks
Custom blocks are elements that users can drag and drop into the rich editor. You can define custom blocks that user can insert into the rich editor using thecustomBlocks() method:
Filament\Forms\Components\RichEditor\RichContentCustomBlock class. The getId() method should return a unique identifier for the block, and the getLabel() method should return the label that will be displayed in the editor’s side panel:
configureEditorAction() method to configure the modal that will be opened when the block is inserted:
schema() method on the action can define form fields that will be displayed in the modal. When the user submits the form, the form data will be saved as “configuration” for that block.
Rendering a preview for a custom block
Once a block is inserted into the editor, you may define a “preview” for it using thetoPreviewHtml() method. This method should return a string of HTML that will be displayed in the editor when the block is inserted, allowing users to see what the block will look like before they save it. You can access the $config for the block in this method, which contains the data that was submitted in the modal when the block was inserted:
getPreviewLabel() can be defined if you would like to customize the label that is displayed above the preview in the editor. By default, it will use the label defined in the getLabel() method, but the getPreviewLabel() is able to access the $config for the block, allowing you to display dynamic information in the label:
Rendering content with custom blocks
When rendering the rich content, you can pass the array of custom blocks to theRichContentRenderer to ensure that the blocks are rendered correctly:
toHtml() method that returns the HTML that should be rendered for that block:
toHtml() method receives two parameters: $config, which contains the configuration data submitted in the modal when the block was inserted, and $data, which contains any additional data that may be needed to render the block. This allows you to access the configuration data and render the block accordingly. The data can be passed in the customBlocks() method:
Grouping custom blocks
You can organize custom blocks into groups using string keys in thecustomBlocks() array. Blocks passed directly (without a string key) are ungrouped and appear first in the panel:
RichContentRenderer. Groups are ignored during rendering — only the block classes are used:
Opening the custom blocks panel by default
If you want the custom blocks panel to be open by default when the rich editor is loaded, you can use theactivePanel('customBlocks') method:
Styling custom block previews with prose
By default, custom block previews are displayed without prose styling to make styling easier. You can enable prose styling for a block’s preview using theshouldApplyProseStylingToPreview() method. This is useful when you want the preview to display with typography styles like headings, paragraphs, and other prose elements:
shouldApplyProseStylingToPreview() returns true, the block’s preview will be styled with the prose typography styles defined in the rich editor, including proper margins, font sizes, and other text formatting. By default, this method returns false, so previews are displayed with minimal styling.
You can make this decision based on the block’s configuration, allowing different blocks to have different preview styling:
Using merge tags
Merge tags allow the user to insert “placeholders” into their rich content, which can be replaced with dynamic values when the content is rendered. This is useful for inserting things like the current user’s name, or the current date. To register merge tags on an editor, use themergeTags() method:
{{ name }}. When the content is rendered, these tags will be replaced with the corresponding values.
To insert a merge tag into the content, users can start typing {{ to search for a tag to insert. Alternatively, they can click on the “merge tags” tool in the editor’s toolbar, which opens a panel containing all the merge tags. They can then drag a merge tag from the editor’s side panel into the content or click to insert it.
Rendering content with merge tags
When rendering the rich content, you can pass an array of values to replace the merge tags with:Using HTML content in merge tags
By default, merge tags render their values as plain text. However, you can render HTML content in merge tags by providing values that implement Laravel’sHtmlable interface. This is useful for inserting formatted content, links, or other HTML elements:
Htmlable interface (such as HtmlString), the system automatically detects this and renders the HTML content without escaping it. Non-Htmlable values continue to be rendered as plain text for security.
Using custom merge tag labels
You may provide custom labels for merge tags that will be displayed in the editor’s side panel and content preview using an associative array where the keys are the merge tag names and the values are the labels:Opening the merge tags panel by default
If you want the merge tags panel to be open by default when the rich editor is loaded, you can use theactivePanel('mergeTags') method:
Using mentions
Mentions allow users to insert references to other records (such as users, issues, or tags) by typing a trigger character. When the user types a trigger character like@, a dropdown appears allowing them to search and select from available options. The selected mention is inserted as a non-editable inline token.
To register mentions on an editor, use the mentions() method with one or more MentionProvider instances:
make()) that activates the mention search. You can have multiple providers with different triggers:
Searching mentions from the database
For large datasets, you should fetch results dynamically usinggetSearchResultsUsing(). The callback receives the search term and should return an array of options with the format [id => label].
When using dynamic search results, only the mention’s id is stored in the content. To display the correct label when the content is loaded, you must also provide getLabelsUsing(). This callback receives an array of IDs and should return an array with the format [id => label]:
Rendering content with mentions
When rendering the rich content, you can pass the array of mention providers to theRichContentRenderer to ensure that the mentions are rendered correctly.
You can make mentions link to a URL when rendered using the url() method. The callback receives the mention’s id and label, and should return a URL string:
Registering rich content attributes
There are elements of the rich editor configuration that apply to both the editor and the renderer. For example, if you are using private images, custom blocks, merge tags, mentions, or plugins, you need to ensure that the same configuration is used in both places. To do this, Filament provides you with a way to register rich content attributes that can be used in both the editor and the renderer. If a plugin implementsHasFileAttachmentProvider, the file attachment provider is automatically resolved from the plugin, so you do not need to call fileAttachmentProvider() on the attribute or on the renderer.
To register rich content attributes on an Eloquent model, you should use the InteractsWithRichContent trait and implement the HasRichContent interface. This allows you to register the attributes in the setUpRichContent() method:
RichEditor component, the configuration registered for the corresponding attribute will be used:
renderRichContent() method on the model, passing the name of the attribute:
Htmlable object to render without escaping the HTML:
Extending the rich editor
You can create plugins for the rich editor, which allow you to add custom TipTap extensions to the editor and renderer, as well as custom toolbar buttons. Create a new class that implements theRichContentPlugin interface:
plugins() method to register your plugin with the rich editor and rich content renderer:
Enabling or disabling toolbar buttons from a plugin
By default, when a plugin provides tools viagetEditorTools(), those tools are registered but not automatically shown in the toolbar. The user needs to manually add them using toolbarButtons() or enableToolbarButtons().
If you want your plugin to automatically enable or disable toolbar buttons, you can implement the HasToolbarButtons interface alongside RichContentPlugin. This is an optional, separate interface:
getEnabledToolbarButtons() method returns button names to add to the toolbar. The getDisabledToolbarButtons() method returns button names to remove from the toolbar.
Plugin toolbar modifications are applied before user-level modifications. This means the user can always override the plugin’s behavior using enableToolbarButtons() or disableToolbarButtons():
Setting up a TipTap JavaScript extension
Filament is able to asynchronously load JavaScript extensions for TipTap. To do this, you need to create a JavaScript file that contains the extension, and register it in thegetTipTapJsExtensions() method of your plugin.
For instance, if you wanted to use the TipTap highlight extension, make sure it is installed first:
highlight.js in the resources/js/filament/rich-content-plugins directory, and add the following code to it:
npm:
bin/build.js:
resources/js/filament/rich-content-plugins/highlight.js into resources/js/dist/filament/rich-content-plugins/highlight.js. You can change these paths to suit your needs. You can compile as many files as you want.
To run the script and compile this file into resources/js/dist/filament/rich-content-plugins/highlight.js run the following command:
boot() method of a service provider, like AppServiceProvider, and use loadedOnRequest() so that it is not downloaded until the rich editor is loaded on a page:
/public directory of your app so that it can be served, you can use the filament:assets command:
getTipTapJsExtensions() method should return the path to the JavaScript file you just created. Now that it’s registered with FilamentAsset, you can use the getScriptSrc() method to get the URL to the file:
Sharing the bundled TipTap/ProseMirror instance
When custom JavaScript extensions import from@tiptap/core or @tiptap/pm/*, each compiled extension includes its own copy of these packages. This wastes around 150-200 KB per extension and — more importantly — creates multiple ProseMirror instances on the page. Because ProseMirror relies heavily on instanceof checks (for Node, Mark, Plugin, DecorationSet, etc.), extensions that bundle their own copy of these modules can fail to interoperate with the editor’s core.
To avoid this, Filament exposes the bundled TipTap and ProseMirror modules on window.FilamentRichEditor.tiptap:
@tiptap/core and @tiptap/pm/{state,view,model} and resolve them from the global at runtime. This lets you keep writing normal import statements in your extension source — other @tiptap/* packages (like @tiptap/extension-highlight) continue to be bundled as usual. The following esbuild plugin inspects each intercepted package’s real named exports at build time and rewrites the imports to read from window.FilamentRichEditor.tiptap:
window.FilamentRichEditor.tiptap is assigned when the rich editor bundle loads, which happens before getTipTapJsExtensions() URLs are fetched. If you need to use the modules in a context where the rich editor has not yet loaded, bundle your own copies instead.The esbuild plugin above reads the named exports from your locally-installed @tiptap/core and @tiptap/pm at build time, so keep those versions roughly in sync with the version bundled by Filament — otherwise a newer named export referenced in your extension may be undefined at runtime.