Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Livewire Quill Text Editor Laravel Package

dasundev/livewire-quill-text-editor

A Livewire rich text editor component built on top of Quill, bringing a powerful WYSIWYG field to Laravel apps. It integrates cleanly into Livewire forms and Blade views, with simple Composer installation and clear documentation.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin immediately. One command, then check what Composer picked:

composer require dasundev/livewire-quill-text-editor

The major version you land on is decided by your Livewire version, and the mapping matters:

Your app Use Why
Livewire 4 ^2.0 v2.0.0 was cut specifically for Livewire 4 (PR #19)
Livewire 3 ^1.0.4 v2.0.0 is a major bump; verify its composer.json still allows ^3.0 before letting it resolve

Don't leave the constraint floating. A future composer update should not be able to move you across a major of an editor component on a Friday afternoon.

Read the published Blade view before writing any code. The package is a self-contained Blade component, and the view is the API surface: it shows the hidden input that carries state into wire:model, the exact tag name, which Quill modules and toolbar buttons are configured by default, and whether assets are registered by a service provider or expected in your layout. The README does not spell out the tag (it may be <x-quill-text-editor>, a namespaced variant, or a @livewire call) — confirm it there rather than guessing. If the docs describe a vendor:publish step for views or config, run it first.

Confirm Quill's CSS and JS actually load. The canonical failure mode for every Quill wrapper is an editor that works but looks like an unformatted box, or one that never initialises because the script 404'd. Keep @livewireStyles, your Vite entry, and any package asset directive in place, then check the network tab for the Quill stylesheet and script before you debug anything else.

First use case: a blog post body. A Post with a longText column, an admin form with title/slug/content, and a public view that renders the stored HTML. It's an hour of work and it exercises everything you'll actually depend on — two-way binding, persistence, validation, and output escaping.

public ?string $content = null;   // null-friendly since v1.0.1
<x-quill-text-editor wire:model="content" />   {{-- confirm tag name in the published view --}}

Escalation order when something misbehaves: published Blade view → browser console → network tab → package changelog → the two or three source files. With zero dependents and 38 stars, you are effectively the primary consumer; reading the source is faster than waiting on an issue.

Implementation Patterns

Wrap it once, then never use the raw tag again. A thin Blade component owns the label, the error slot, and your toolbar preset, so changing the toolbar across the app is one file instead of a grep and a prayer:

{{-- resources/views/components/forms/rich-text.blade.php --}}
<div @if($key) wire:key="rte-{{ $key }}" @endif>
    @if ($label)<label class="block font-medium">{{ $label }}</label>@endif
    <x-quill-text-editor wire:model="{{ $name }}" />
    @error($name) <span class="text-sm text-red-600">{{ $message }}</span> @enderror
</div>

Validation stays completely ordinary. Rules on the property, $this->validate() in save(). The editor is not a special case — it's a string property behind a fancy input. Don't reach for custom rules.

Sanitize on write, not on read. The package does not purify HTML. Put HTMLPurifier or mews/purifier in a model mutator or form-object hook so every write path is covered — admin form, API, importer, seeder. Then {!! $post->content !!} in the frontend becomes a deliberate, auditable decision rather than a latent XSS.

Multiple editors work from v1.0.3, but give each a stable identity. Bind content and excerpt to separate properties, render the tags in a consistent order, and put a wire:key on each wrapper. Livewire's DOM morphing is what kills editor instances, and a key is what stops one editor's DOM being recycled into another's.

Default to deferred wire:model. Round-tripping every keystroke to the server is a latency tax on desktop and unusable on mobile. If you need a live word count or preview, use wire:model.live.debounce.750ms — and even then, compute the count client-side so the UI doesn't wait for the server.

Preview panes need wire:ignore. If a preview toggle re-renders the parent, the editor subtree gets morphed and the user loses cursor position and scroll — reported as "the editor jumps." Keep the editor out of the re-render path.

Images go through Livewire's existing upload flow, not a new mechanism: intercept the toolbar's image button in your wrapper's JS, hand the file to $wire.upload(...), and on completion insert the URL with quill.insertEmbed(index, 'image', url). Put that JS in your wrapper view as an extension point — don't fork the package to add a feature.

Store HTML; capture Delta only if you need it. Quill's native format is Delta (JSON). If search, diffing, or email re-formatting needs structure, listen for the change event, copy the Delta into a second property, and persist both columns. Rebuilding Delta from HTML later is lossy.

Programmatic updates need an explicit test. v1.0.1 fixed "display the model value on first load," which only proves initial hydration. It does not promise that $this->content = $html from PHP pushes into an already-mounted Quill instance. Wire up a "reset to template" button and click it. If it doesn't sync, remount with a changing wire:key or set quill.root.innerHTML from a dispatched browser event.

Testing. Livewire::test(PostForm::class)->set('content', '<p>Hello</p>')->call('save')->assertHasNoErrors()->assertDatabaseHas(...) covers validation and persistence. Quill's JS never runs in PHPUnit, so don't assert editor behaviour there — that's Dusk/Playwright territory and usually not worth the maintenance.

Admin panels need glue. There is no Filament or Nova field adapter. In Filament, use a ->view() field or a custom form component and render this Blade component inside it, handling state sync in afterStateUpdated. Roughly 20 lines per field — budget honestly rather than expecting a drop-in.

Gotchas and Tips

Set a floor of ^1.0.3 and mean it. v1.0.1 fixed null handling and first-load value display; v1.0.3 fixed "the editor can only be used once" and multiple editors per component. Anything below that knowingly reintroduces solved bugs. Also note v1.0.4 shipped with no changelog entry — diff it yourself if you care.

v2.0.0 is a major bump with no documented breaking changes, which is not the same as having none. One PR, a first-time contributor, a full major. Before upgrading, diff v1.0.4...v2.0.0 and look at four things specifically: the composer.json constraint (^3.0|^4.0 vs ^4.0-only), the service provider's asset registration, the published view, and any config keys. If it's a hard cutover to Livewire 4, treat it as a coordinated migration, not a composer update.

Normalize the empty state. Quill's empty document is <p><br></p> — truthy, non-null, and not ''. If your column is nullable, strip tags at save time and set null when nothing remains. Otherwise every "empty" record has content and every @if ($post->content) branch in your app lies.

When the editor dies after a parent re-render, reach for wire:ignore plus wire:key. Symptom: you type, something else on the page updates, and the editor blanks or stops accepting input. Cause: Livewire morphs the node out from under Quill. The package handles common cases internally, but a keyed wrapper is your safety net.

No asset compilation means the Quill assets come from somewhere you don't control — typically a CDN. Check what the service provider or view registers. If your app has a strict CSP, runs in an air-gapped environment, or must work offline, plan to vendor the Quill CSS/JS yourself and point the view at your local copies.

Check Tailwind Preflight against Quill. @tailwind base resets headings, lists, and blockquotes, which can make the editor content look different from your published output. Verify that what the author sees in the editor matches the rendered post; if not, scope your prose styles (@tailwindcss/typography's prose class on the public view is the usual fix).

wire:model on a rich text field posts HTML on every sync. Large documents with base64 images will hit post_max_size fast — store images as files, not inline data URIs.

Keep an eye out for Livewire 4 changes to wire:model semantics (deferred-by-default behaviour, .live modifiers) if you migrate. The component's internal binding is written against a specific Livewire version, and "it still renders" is not proof the sync direction still works — test both typing and programmatic resets after any Livewire upgrade.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core