joelwmale/livewire-quill
A Laravel Livewire component for the Quill WYSIWYG editor. Add rich text fields to Livewire forms, bind content with wire:model, customize the toolbar, and get clean HTML output without writing JavaScript.
This package wraps the Quill rich-text editor as a Livewire component, so the fastest path is: install it, confirm which component alias it registers, then bind it to a model property you already round-trip to the database.
composer require joelwmale/livewire-quilllivewire-quill:config and livewire-quill:assets). If the asset tag doesn't exist in your version, check the package's README and src/LivewireQuillServiceProvider.php for the exact tag names before assuming something is broken.<head> (or the package's Blade directives if your version ships them). A completely unstyled editor usually means missing Quill CSS, not a broken component.<form wire:submit="save">
<livewire:quill-editor wire:model="content" />
@error('content') <span>{{ $message }}</span> @enderror
<button type="submit">Save</button>
</form>
Confirm the actual tag with php artisan livewire:list (or livewire:list equivalent in Livewire 4) rather than copying a tag from a blog post — the component name has changed across major versions, and a wrong alias gives you a confusing "component not found" rather than a helpful error.
v5.4.0 specifically: this release is additive — livewire/livewire is widened to ^3.0|^4.0 and Livewire 4 is now covered in CI. There are no breaking changes and no migration steps. If you were pinned to Livewire 3 solely because of this package, you can now upgrade Livewire first and the editor will keep working. Note the CI matrix: PHP 8.2–8.4 is tested against both Livewire 3 and 4, but PHP 8.5 is only tested against Livewire 4 — if you're on 8.5, treat Livewire 4 as required, not optional.
First real use case: a CMS/Article $content field on an edit form, with server-side sanitisation on save (see Gotchas).
Treat the editor as an uncontrolled HTML field, not a Livewire-owned input. Quill mutates its own DOM; Livewire's morphing wants to own it. The reliable pattern is to wrap in wire:ignore and let the component bridge changes into Livewire:
<div wire:ignore>
<livewire:quill-editor wire:model="content" />
</div>
Without this, any parent re-render can wipe the editor contents or reset the cursor mid-typing.
Choose your binding mode deliberately. wire:model (deferred) is correct for most forms — content syncs on the next Livewire request. wire:model.live will fire a round trip on every keystroke/change event, which is expensive for a large HTML payload. If you need live validation, prefer .live.debounce.500ms or .blur. Reserve truly-live binding for word-count/preview panels where the latency is user-visible anyway.
Put it in a dedicated form component, not inline in a Blade page. A ArticleForm Livewire component with public string $content = '', rules() and save() keeps the editor's state lifecycle contained and makes the editor testable independently of page layout. Pass initial content via mount():
public function mount(?Article $article = null): void
{
$this->content = $article?->content ?? '';
}
Key the component when you swap records. If you reuse the same form component across multiple articles (e.g. a slide-over), add wire:key="editor-{{ $article->id }}" on the wrapper. Without it, Quill keeps the previous document's content because it never re-initialises.
Image uploads are a configuration exercise, not code. The package's config file is where you set the upload disk, path and validation (size/mime). Publish the config, point it at your public disk, and run php artisan storage:link. If images upload but render as broken links, it's almost always the disk/APP_URL mismatch rather than the editor.
Sanitise on save — always. Quill produces HTML and the package will happily hand you <script>-adjacent markup from a paste. Run content through an allowlist sanitizer (e.g. HTMLPurifier via mews/purifier) in the component's save() or, better, in a form request / model mutator so every write path is covered:
$this->validate();
$article->update(['content' => clean($this->content)]);
Store HTML, not Delta. Persist the HTML representation in a text/longText column so the same content renders on the front end without needing Quill on the read path. Storing Delta JSON locks you into re-hydrating through the editor.
Testing: use Livewire::test(ArticleForm::class)->set('content', '<p>Hello</p>')->call('save') and assert the DB value. The editor's JS bridge is not exercised in these tests, which is fine — you're testing your state handling, and that's where the bugs actually are. Add one browser/Dusk-level smoke test that types into the editor if the editor's wiring is business-critical.
wire:ignore is the number one support issue. If content "randomly resets" or "jumps to the top while typing", you're missing it, or you have it in the wrong place (it must wrap the editor, not sit inside it).wire:key, Quill instances can stack. Symptom: duplicate toolbars or duplicated typing. Fix with keys, not with JS hacks.php artisan config:clear after editing config/livewire-quill.php — a cached config in a container is a classic "my toolbar change did nothing".@livewireStyles/@livewireScripts ordering. Quill must not load before Livewire's Alpine/Livewire runtime in a way that breaks the component's init. If the editor renders but is inert, check script order first, then the browser console.max:65535 on a longText is fine; a text column truncating silently is not) and avoid .live binding on long-form fields.@error block in every version — add @error('content') in your own Blade around the component.'<p><br></p>', not ''. Quill normalises empty content. Your required rule may pass on a visually empty editor — normalise/trim-and-strip-tags before validating if an empty body must be rejected.^5.4 is still safe (constraint is ^3.0|^4.0), but pin consciously — future releases may not maintain the 3.x leg, and this release is the moment that risk becomes visible in the CI matrix.How can I help you explore Laravel packages today?