Trix is a lightweight rich text editor from Basecamp. In Laravel 13 you install it with npm, add a hidden input and a <trix-editor> element to your form, and Trix writes the HTML into that input so Laravel can save it like any other field. The important part is security: HTML written by users must be cleaned before you print it. This tutorial covers installation, saving, file attachments and sanitizing.
Targets: Laravel 13 (PHP 8.3 to 8.5), Vite and the trix npm package.
Step 1: Install Trix
npm install trix// resources/js/app.js
import 'trix';
import 'trix/dist/trix.css';Rebuild with npm run dev or npm run build, and make sure your layout loads resources/js/app.js with @vite.
Step 2: Add the editor to a form
<form method="POST" action="{{ route('articles.store') }}">
@csrf
<input type="text" name="title" value="{{ old('title') }}">
<input id="body" type="hidden" name="body" value="{{ old('body') }}">
<trix-editor input="body"></trix-editor>
@error('body') <p>{{ $message }}</p> @enderror
<button type="submit">Publish</button>
</form>The input="body" attribute connects the editor to the hidden input with the same id. On the edit page, put the saved HTML into the hidden input's value attribute with {{ old('body', $article->body) }}. Blade escapes the HTML for the attribute, and Trix reads it as HTML.
Step 3: Validate and save
public function store(Request $request): RedirectResponse
{
$data = $request->validate([
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string', 'max:100000'],
]);
Article::create($data);
return redirect()->route('articles.index')->with('success', 'Article saved.');
}Treat the body value as untrusted HTML. A user can send any string to that field without using the editor at all.
Step 4: Sanitize before output
Printing the saved HTML with {!! $article->body !!} is dangerous. If an attacker saves a script tag or an onerror attribute, it runs in every visitor's browser (a stored cross-site scripting attack). Clean the HTML with an HTML purifier library, either before saving or before displaying.
One option is the stevebauman/purify package, which wraps HTMLPurifier. Check its Packagist page for the versions that support Laravel 13 before installing:
composer require stevebauman/purifyuse Stevebauman\Purify\Facades\Purify;
$data['body'] = Purify::clean($data['body']);Configure the allowed tags so they match what Trix produces: bold, italic, strike, links, headings, quotes, code, lists and line breaks, plus the figure and image markup if you allow attachments. Sanitizing on save keeps cleaned data in the database. Sanitizing again on output is an extra safety layer if you also import content from other sources.
Step 5: Handle image and file attachments
By default Trix lets users drop files, but it does not upload them. You must listen to the trix-attachment-add event, upload the file and tell Trix the final URL:
document.addEventListener('trix-attachment-add', (event) => {
const attachment = event.attachment;
if (!attachment.file) return;
const form = new FormData();
form.append('file', attachment.file);
const request = new XMLHttpRequest();
request.open('POST', '/trix-uploads');
request.setRequestHeader('X-CSRF-TOKEN', document.querySelector('meta[name="csrf-token"]').content);
request.setRequestHeader('Accept', 'application/json');
request.upload.addEventListener('progress', (progress) => {
attachment.setUploadProgress((progress.loaded / progress.total) * 100);
});
request.addEventListener('load', () => {
if (request.status === 201) {
const response = JSON.parse(request.responseText);
attachment.setAttributes({ url: response.url, href: response.url });
}
});
request.send(form);
});Add the CSRF meta tag to your layout. The controller validates and stores the image and returns JSON with a URL:
public function store(Request $request): JsonResponse
{
$request->validate([
'file' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:4096'],
]);
$path = $request->file('file')->store('trix', 'public');
return response()->json(['url' => asset('storage/'.$path)], 201);
}Register the route with the auth middleware: Route::post('/trix-uploads', [TrixUploadController::class, 'store'])->middleware('auth');. Run php artisan storage:link so the image URL works.
How do I show the saved content?
<div class="trix-content">
{!! $article->body !!}
</div>Only use {!! !!} if the body was sanitized as shown. The trix-content class applies Trix's content styles from its CSS file, so headings, lists and quotes look the same as in the editor.
How do I check that it works?
- Open the form and confirm the toolbar appears. Write a heading, bold text and a list.
- Submit and check the database: the body column should contain HTML.
- Edit the article and confirm the content loads back into the editor.
- Try saving
<script>alert(1)</script>through a direct request and confirm it does not run on the display page.
Common mistakes
- Printing the HTML without sanitizing: this opens the door to XSS.
- Hidden input id mismatch: the
inputattribute oftrix-editormust equal the input's id. - No upload handler: dropped images show an upload error until you handle the event.
- Missing CSS import: the toolbar looks broken.
- Allowing any file type: validate uploads on the server.
FAQ
Is Trix better than Quill or CKEditor?
It is simpler and lighter, with fewer features. Choose based on what your users need.
Does Trix save Markdown?
No. It saves HTML. If you need Markdown, use a Markdown editor.
Can I use Trix in Livewire?
Yes, but synchronizing the editor with Livewire needs extra JavaScript that listens to the trix-change event and updates the property.



