Rich Text
The RichText field renders a WYSIWYG editor with a configurable toolbar. It submits an HTML string, or null when the editor is empty. The PHP field adds a string rule automatically, so optional editors should also call nullable().
Basic Usage
use InertiaUI\Forms\Fields\RichText;
RichText::make('body')->nullable();This renders an editor with the default toolbar: bold, italic, underline, strike, an H2 heading, lists, links, blockquotes, inline code, undo, and redo.
Toolbar Presets
Simple
A minimal toolbar with just bold, italic, and link:
RichText::make('bio')->simple();Full
The full preset adds the larger editor controls, including image embeds, task lists, and text alignment:
RichText::make('content')->full();Custom
Pass your own toolbar configuration:
RichText::make('notes')
->toolbar('bold italic | bullet ordered ~ link undo redo');You may also pass an array. Each nested array becomes a visual button group:
RichText::make('notes')->toolbar([
['bold', 'italic'],
['bulletList', 'orderedList'],
['link'],
]);Flat arrays are accepted too:
RichText::make('notes')->toolbar([
'bold', 'italic', '|', 'bulletList', 'orderedList', '|', 'link',
]);Use | as a visual separator between button groups and ~ as a flexible spacer. The string grammar also accepts these aliases: bullet for bulletList, ordered for orderedList, quote for blockquote, hr for horizontalRule, and h1, h2, or h3 for heading.
RichText::normalizeToolbar() exposes the same normalization when application code needs the canonical flat toolbar array without building a field instance.
h1, h2, and h3 all normalize to the single heading toolbar control. That control toggles an H2 heading; it is not a heading-level picker.
Available Toolbar Items
| Item | Description |
|---|---|
bold | Bold text |
italic | Italic text |
underline | Underlined text |
strike | Strikethrough text |
heading | Toggle an H2 heading |
highlight | Highlighted text |
subscript | Subscript text |
superscript | Superscript text |
bulletList | Unordered list |
orderedList | Numbered list |
taskList | Checkbox list |
link | Hyperlink |
image | Image embed |
blockquote | Block quote |
code | Inline code |
codeBlock | Code block |
horizontalRule | Horizontal divider |
alignLeft | Left alignment |
alignCenter | Center alignment |
alignRight | Right alignment |
undo | Undo |
redo | Redo |
Link Options
The link toolbar item opens a native modal dialog for inserting, editing, and removing links. Configure the generated link policy from PHP:
RichText::make('body')
->simple()
->linkTargetBlank()
->linkNoopener()
->linkNoreferrer()
->linkAllowedProtocols(['https', 'mailto'])
->linkDefaultProtocol('https');By default, links open in the same tab, noopener is enabled for links that open in a new tab, noreferrer is disabled, auto-linking and link-on-paste are enabled, the default protocol is http, and allowed protocols are http, https, mailto, and tel.
Use autoLink(false) to stop converting typed URLs and linkOnPaste(false) to stop converting a pasted URL. These switches do not disable the link dialog. linkAllowedProtocols() normalizes and deduplicates protocol names. Removing the current default protocol makes the first allowed protocol the new default. linkDefaultProtocol() must select a protocol that is already allowed.
Use allowLinkTargetOverride(false) when the field configuration should fully control whether links open in a new tab. With target override allowed, authors may toggle "Open in new tab" per link.
RichText::make('body')
->linkTargetBlank()
->allowLinkTargetOverride(false);Use linkRel() for additional rel tokens such as nofollow, sponsored, or ugc:
RichText::make('body')
->linkRel(['nofollow', 'ugc']);Protocol options are validated when the form is built. Unsafe or malformed protocols such as javascript: and data: are rejected, and the editor also rejects links whose protocol is not allowed. This policy applies to links created or normalized by the editor; it is not a replacement for server-side HTML sanitization when accepting untrusted HTML.
Height
Set minimum and/or maximum heights for the editor area in pixels:
RichText::make('content')
->minHeight(200)
->maxHeight(600);Image Embeds
The image toolbar item accepts image URLs by default. Add imageUploads() when authors should also be able to pick an image file from their device:
use InertiaUI\Forms\Fields\RichText;
use InertiaUI\Forms\FileUploads\UploadConfig;
RichText::make('body')
->full()
->imageUploads(fn (UploadConfig $uploads) => $uploads
->maxSize(2048)
->maxDimensions(width: 2400, height: 1600));RichText image uploads use the same temporary upload pipeline as the File field. Configure custom routes, chunking, direct-to-storage uploads, upload validators, and token validation in File Uploads. Common image validation helpers such as accept(), maxSize(), minSize(), dimensions(), minDimensions(), and maxDimensions() run during the upload request. The upload config always includes Laravel's image rule; accept() narrows the allowed image MIME types and must not turn this into a general file upload.
The generated companion field is <name>_images, for example body_images. Use getImageUploadFieldName() on the field or RichTextUploads::imageUploadFieldName('body') when application code needs that name explicitly.
storeWithForm() is not supported for RichText images. RichText still submits an HTML string for the field itself. Uploaded images are tracked in a companion token list, so your controller may resolve the temporary uploads and rewrite the HTML before storing it.
Image Resizing
Images inserted by URL or uploaded through imageUploads() may be resized in the editor with corner handles. The editor stores the user's resized display size as numeric width and height attributes on the <img> tag, so the submitted value stays regular HTML. RichTextUploads and RichTextContent preserve those attributes when they rewrite temporary upload markers or stored-image markers.
Storage and rendering callbacks receive the existing image attributes through the RichTextImage builder. Use RichTextImage::width(), height(), or dimensions() when your application wants to normalize or override the user's resized display size while storing or rendering an image. These fluent setters write the rendered <img width height> attributes. They are not getters and do not describe the source image's intrinsic pixel dimensions. Use upload validation helpers such as UploadConfig::dimensions() and maxDimensions() when you need to validate the actual uploaded file.
Store Uploaded Images
Use RichTextUploads in your controller or form request handler. The helper compares the data-inertia-forms-upload markers in the submitted HTML with the companion token list before storage callbacks run. It then passes each validated SubmittedUpload to your storage callback with a RichTextImage builder, rewrites the matching <img> tags, and deletes the temporary uploads after the rewrite succeeds:
use InertiaUI\Forms\FileUploads\SubmittedUpload;
use InertiaUI\Forms\RichText\RichTextImage;
use InertiaUI\Forms\RichText\RichTextUploads;
$html = RichTextUploads::from($request, 'body')
->storeImagesUsing(function (SubmittedUpload $upload, RichTextImage $image): RichTextImage {
$storedImage = app(ContentImageStore::class)->store($upload->getUploadedFile());
return $image
->src($storedImage->url())
->identifier($storedImage->uuid(), [
'id' => $storedImage->id(),
]);
})
->toHtml();The callback must return the provided RichTextImage builder, a new RichTextImage instance, or null to keep the builder as-is. Returning arrays is intentionally unsupported so the replacement shape stays typed and readable.
RichTextImage may set src(), alt(), title(), width(), height(), or both dimensions together. attribute() and attributes() set other syntactically valid image attributes; pass null or false to remove one. identifier() writes a durable application-owned identifier plus optional metadata for deferred rendering, and toAttributes() returns the final attribute map.
Spatie Media Library
For models that use Spatie Media Library, use the convenience helper:
use InertiaUI\Forms\RichText\RichTextUploads;
$html = RichTextUploads::from($request, 'body')
->storeImagesInMediaLibrary($post, 'content-images')
->toHtml();The helper stores each uploaded image in the given media collection, rewrites the image src to the media URL, and records a generic stored-image identifier so the HTML may also be resolved later. Its optional third disk argument overrides the media collection's storage disk. Leave it empty to use the collection's configured disk.
Deferred Image Rendering
Use keepTokenized() when the stored HTML should keep app-owned image references instead of final URLs. This is useful when responsive images are generated on a queue, when image URLs should be created during rendering, or when the final URL depends on the current tenant or viewer.
use InertiaUI\Forms\RichText\RichTextUploads;
$post->body = RichTextUploads::from($request, 'body')
->keepTokenized()
->storeImagesInMediaLibrary($post, 'content-images')
->toHtml();Later, replace stored image markers while rendering:
use InertiaUI\Forms\RichText\RichTextContent;
use InertiaUI\Forms\RichText\RichTextImage;
use InertiaUI\Forms\RichText\RichTextStoredImage;
$html = RichTextContent::from($post->body)
->replaceImagesUsing(function (RichTextStoredImage $stored, RichTextImage $image): RichTextImage {
return $image->src(route('content-images.show', $stored->identifier()));
})
->toHtml();The RichTextStoredImage passed to the callback exposes identifier(), all metadata(), a nested meta($key, $default) lookup, and the original image attributes(). Returning null from the render callback keeps the original tokenized image unchanged. RichTextContent::toHtml() returns the rewritten HTML string or null when its input was null.
Stored images use the package-owned data-inertia-forms-image marker as a durable application reference. The marker stores the identifier and optional metadata returned by your storage callback or Media Library helper, not the temporary upload token. Treat those identifiers as application data: resolve and authorize them in your RichTextContent::from(...)->replaceImagesUsing(...) callback before rendering a URL.
Continue to sanitize untrusted HTML according to your application's policy before storing or rendering it. Image upload validation only validates the uploaded files; it does not sanitize arbitrary HTML submitted in the RichText value. Sanitizers that run before RichTextUploads should allow supported image attributes, usually src, alt, title, width, height, and data-inertia-forms-upload. Tokenized stored images also need data-inertia-forms-image before RichTextContent renders them.
Word and Character Count
Word Count
RichText::make('content')->showWordCount();Character Count
RichText::make('content')->showCharacterCount();Max Length
Set a character limit for the editor text:
RichText::make('content')
->maxLength(5000)
->showCharacterCount();maxLength() is serialized to the editor and configures Tiptap's CharacterCount extension. It limits the visible text the editor counts, and showCharacterCount() makes that count visible to the user.
It does not add a Laravel validation rule by itself. RichText submits an HTML string, or null for an empty editor, so Laravel string length rules measure the submitted markup as well as the visible text. Add your own server-side rules for the submitted value, sanitize untrusted HTML before storing or rendering it, and use a custom rule or FormRequest hook if the server must enforce a visible-text limit:
RichText::make('content')
->maxLength(5000)
->showCharacterCount()
->rules(['nullable', 'string']);$request->validate([
'content' => ['nullable', 'string'],
]);
$html = $request->string('content')->toString();
// Sanitize the HTML with your application's approved sanitizer before storing.
// Count text extracted from sanitized HTML when enforcing a visible-text limit.Article Editor Configuration
Use placeholder() to set the empty-editor prompt:
RichText::make('body')
->label('Article Body')
->placeholder('Start writing...')
->full()
->linkAllowedProtocols(['https', 'mailto'])
->linkDefaultProtocol('https')
->linkTargetBlank()
->linkNoopener()
->minHeight(300)
->maxHeight(800)
->showWordCount()
->showCharacterCount()
->maxLength(10000)
->required();RichText-specific methods above compose with shared field APIs. Use Form Class, Model Binding, Validation, Conditional Visibility, Authorization, and Styling for labels, help, defaults, rules, visibility, authorization, layout, and classes. RichText image uploads use the same runtime configuration described in File Uploads.
Using the Component Directly
You may render RichText from each stack's richtext subpath with local HTML state, or pass serialized field props during manual form rendering.
<script setup lang="ts">
import { ref } from 'vue'
import {
RichText,
type RichTextLinkOptions,
type RichTextToolbar,
} from '@inertiaui/form-vue/richtext'
const body = ref<string | null>('<p>Draft body</p>')
const locked = ref(false)
const hasError = ref(false)
const toolbar: RichTextToolbar = [
'bold',
'italic',
'|',
'bulletList',
'orderedList',
'|',
'link',
'image',
'|',
'undo',
'redo',
]
const linkOptions: RichTextLinkOptions = {
targetBlank: true,
noopener: true,
noreferrer: true,
rel: ['nofollow'],
allowedProtocols: ['https', 'mailto'],
defaultProtocol: 'https',
autoLink: true,
linkOnPaste: true,
allowTargetOverride: false,
}
const editorParts = {
toolbar: 'border-b border-zinc-200',
toolbarButton: 'data-[active=true]:text-blue-600',
editor: 'min-h-48',
footer: 'text-xs',
dialog: 'max-w-lg',
}
</script>
<template>
<RichText
v-model="body"
id="article-body"
name="body"
label="Article body"
help="Draft the public article body."
placeholder="Start writing..."
:toolbar="toolbar"
:min-height="300"
:max-height="800"
show-word-count
show-character-count
:max-length="10000"
:link-options="linkOptions"
required
precognitive
:disabled="locked"
:invalid="hasError"
:error="hasError ? 'Article body is required.' : null"
badge="Draft"
badge-class="bg-zinc-100 text-zinc-700"
label-trailing="Required"
label-trailing-class="text-zinc-500"
tooltip="Shown on the article page."
help-position="below"
layout="stacked"
control-position="end"
label-class="font-medium"
help-class="text-zinc-500"
error-class="text-red-600"
wrapper-class="max-w-3xl"
control-class="bg-white"
class="font-medium"
:part-classes="editorParts"
/>
</template>import { useState } from 'react'
import {
RichText,
type RichTextLinkOptions,
type RichTextToolbar,
} from '@inertiaui/form-react/richtext'
const toolbar: RichTextToolbar = [
'bold',
'italic',
'|',
'bulletList',
'orderedList',
'|',
'link',
'image',
'|',
'undo',
'redo',
]
const linkOptions: RichTextLinkOptions = {
targetBlank: true,
noopener: true,
noreferrer: true,
rel: ['nofollow'],
allowedProtocols: ['https', 'mailto'],
defaultProtocol: 'https',
autoLink: true,
linkOnPaste: true,
allowTargetOverride: false,
}
const editorParts = {
toolbar: 'border-b border-zinc-200',
toolbarButton: 'data-[active=true]:text-blue-600',
editor: 'min-h-48',
footer: 'text-xs',
dialog: 'max-w-lg',
}
export default function ArticleEditor() {
const [body, setBody] = useState<string | null>('<p>Draft body</p>')
const [locked] = useState(false)
const [hasError] = useState(false)
return (
<RichText
value={body}
onValueChange={setBody}
id="article-body"
name="body"
label="Article body"
help="Draft the public article body."
placeholder="Start writing..."
toolbar={toolbar}
minHeight={300}
maxHeight={800}
showWordCount
showCharacterCount
maxLength={10000}
linkOptions={linkOptions}
required
precognitive
disabled={locked}
invalid={hasError}
error={hasError ? 'Article body is required.' : null}
badge="Draft"
badgeClass="bg-zinc-100 text-zinc-700"
labelTrailing="Required"
labelTrailingClass="text-zinc-500"
tooltip="Shown on the article page."
helpPosition="below"
layout="stacked"
controlPosition="end"
labelClass="font-medium"
helpClass="text-zinc-500"
errorClass="text-red-600"
wrapperClass="max-w-3xl"
controlClass="bg-white"
class="font-medium"
partClasses={editorParts}
/>
)
}RichText is a composite editor and does not forward arbitrary native attributes to an inner native element. See Native Attributes And Events. Image-upload runtime configuration uses File Uploads.
Editor Support Components
The richtext subpath also exports EditorShell, EditorToolbar, and EditorCountFooter with their prop types. These low-level presenters let custom RichText integrations reuse the package shell, toolbar semantics, count footer, native attributes, and styling hooks without pulling Tiptap into the root or components entry points.
| Component | Public options |
|---|---|
EditorShell | name writes data-editor-name; focused, invalid, and disabled select semantic states; classValue and controlClass extend shell classes. Vue provides toolbar, default, and footer slots. React renders children. Both forward native wrapper attributes. |
EditorToolbar | items is the normalized control/separator/spacer order; buttons supplies each control's label, state, and action; disabled disables every control; dataAttributePrefix changes generated test/data attribute names; classValue and buttonClass extend toolbar styling. React additionally receives an icons map; Vue toolbar buttons carry their icon components. Native wrapper attributes are forwarded. |
EditorCountFooter | footer.show controls rendering; wordLabel, characterLabel, and characterLimitLabel provide already-formatted display text; classValue extends footer styling. Both implementations forward native wrapper attributes. |
For broader presenter customization, copy the shipped RichText presenter and see Customizing Presenters.