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

Filament Lexical Editor Laravel Package

malzariey/filament-lexical-editor

A Filament form field that brings the Lexical rich text editor to your admin panels. Create clean, modern WYSIWYG content with JSON output, seamless Livewire integration, customizable toolbars, and an intuitive editing experience for Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

This package gives you a LexicalEditor form field for FilamentPHP, wrapping Meta's Lexical rich‑text framework (React-based) behind a Livewire-friendly Blade component. It's a young package (16★, 0 dependents), so treat it as "promising but DIY" rather than a drop-in replacement for Filament's built-in RichEditor.

Install and wire up:

composer require malzariey/filament-lexical-editor
# if the package ships publishable assets:
php artisan vendor:publish --tag="filament-lexical-editor-assets"
php artisan filament:assets

Add the field to any form schema:

use Malzariey\FilamentLexicalEditor\Forms\Components\LexicalEditor;

LexicalEditor::make('content')
    ->label('Body')
    ->columnSpanFull(),

Where to look first (in the repo):

  1. src/Forms/Components/LexicalEditor.php — the public API surface. Everything you can chain (toolbar, height, JSON/HTML mode, etc.) is defined here and is your source of truth.
  2. resources/js/ — the Lexical config (nodes, plugins, toolbar composition). If you need a feature Lexical supports but the PHP side doesn't expose, this is where you'll patch it.
  3. resources/views/ — the Blade shell. Useful for understanding how Livewire binds wire:model to the editor and how content is synced back.
  4. composer.json — check the filament/* constraint to confirm the panel version you're on (v3 vs v4) is supported.

First use case: drop it into a PostResource form replacing RichEditor::make('body'), store the output as JSON in a text/longText column, and render on the public site with a small Blade partial or an accessor. That's the canonical path — build that end-to-end before customising anything.


Implementation Patterns

Storage format — decide up front. Lexical's native serialization is JSON ({"root":{"children":[...]}}). The package almost certainly stores that JSON as a string. If your schema is body (longText), you'll want an accessor for rendering:

// In the model
public function getBodyHtmlAttribute(): string
{
    return $this->body ? app(LexicalRenderer::class)->toHtml($this->body) : '';
}

If the package exposes an HTML output mode (check the component for a ->html()/->outputAsHtml() style method), prefer that — the JSON round-trip through a renderer you have to maintain is a real tax.

Resource form pattern:

public static function form(Form $form): Form
{
    return $form->schema([
        TextInput::make('title')->required(),
        LexicalEditor::make('body')
            ->required()
            ->columnSpanFull()
            ->toolbarButtons(['bold','italic','link','bulletList','orderedList','h2','h3']),
    ]);
}

Rendering on the frontend. Don't try to re-render Lexical server-side. Two pragmatic options:

  • Store JSON and a derived body_html column that you regenerate on save (->afterStateUpdated() or a model observer). Render {!! $post->body_html !!} on the frontend and never touch Lexical JS again.
  • Or use Lexical's @lexical/html on the client to render, but that means shipping the editor bundle to public pages — usually overkill for a CMS field.

Content preview in tables:

Tables\Columns\TextColumn::make('body')
    ->formatStateUsing(fn ($state) => Str::limit(strip_tags($this->lexicalToText($state)), 80)),

Strip tags from the JSON-extracted text, because raw JSON in a table cell is unreadable.

Multiple editors on one page (e.g. tabs for body and summary): generally works, but each instance mounts its own Lexical React root. Watch Livewire re-renders — if you ->reactive() one field and it re-renders the form, the other editors can lose focus. Keep editors non-reactive unless you actually need cross-field reactivity.

Panel-level asset registration. If the package ships a service provider, make sure it's auto-discovered or registered in config/app.php for older Laravel. Filament v3 panels also need filament:assets run after publish, or the editor renders as a blank box.

Validation pattern. Lexical JSON for an "empty" document is not null — it's {"root":{"type":"root","children":[{"type":"paragraph","children":[]}]}}. If you use ->required(), you may want a custom rule that rejects empty-document JSON:

->rules([
    fn (): Closure => function (string $attr, $value, Closure $fail) {
        $decoded = json_decode($value, true);
        if (empty(data_get($decoded, 'root.children.0.children'))) {
            $fail('The body field is required.');
        }
    },
])

Gotchas and Tips

JSON is not HTML — this is the #1 source of pain. Any place you expect HTML (email templates, RSS, search indexing, Str::limit() on a table column) will break until you add a converter. Add the derived-HTML column early rather than discovering this in production.

Empty vs. missing state. As above: a "cleared" editor often posts a structurally-valid but empty JSON blob, not null. Coerce it in your mutator (setBodyAttribute) so your database doesn't accumulate {"root":{"children":[]}} rows that break ->required() semantics and pollute whereNotNull queries.

Assets and Livewire SPA navigation. Filament loads assets once per full page load. If your panel uses SPA-style navigation (Filament v3's ->spa() mode), the editor bundle can be missing on soft navigations. Symptom: the toolbar appears but nothing is editable, or a console error about missing React/@lexical/* globals. Fix: ensure the package's service provider registers assets via FilamentAsset::register()/Filament::registerScripts(), not by injecting <script> into a Blade view.

Version compatibility. Only 16 stars and 0 dependents means Filament v4 (or even a v3 minor) may silently break the field — the PHP component compiles fine but the JS assumes a particular Livewire/React boundary. Pin the package in composer.json and run filament:upgrade checks after any Filament bump. Check the repo's composer.json constraint before you commit.

Styling is on you. Lexical has no opinion about typography. Wrap rendered output in prose prose-zinc dark:prose-invert (Tailwind Typography) or your editor output will be unstyled <p>/<h2> tags next to your beautifully styled app. Inside the editor itself, dark mode needs the package's CSS variables or you'll get black-on-black in the admin panel.

XSS. If you do convert JSON→HTML, run it through Purifier (mews/purifier) or an equivalent before {!! !!}. Lexical's own HTML export is not a sanitizer — users can paste arbitrary HTML into the editor.

Toolbar customisation is the extension point you'll actually use. ->toolbarButtons([...]) (or whatever the component exposes — check LexicalEditor.php) is a safe, versioned API. Going deeper (custom Lexical nodes, mentions, images) means editing resources/js/ and rebuilding the bundle — at that point you're maintaining a fork. Ask: is this worth it vs. Filament's RichEditor (Tiptap) which has a bigger ecosystem?

Debugging approach. Open the browser console first — Lexical errors are almost always JS-side (missing node type in the config, unsupported JSON from a legacy record, a LinkNode in stored JSON that isn't registered in the current toolbar config). If the editor displays but can't save, inspect the Livewire network payload to confirm wire:model is receiving the JSON string, not [object Object].

Testing. Don't try to test the editor in Dusk unless you must. Test the PHP side: assert that saving a valid JSON payload persists, assert your body_html accessor produces expected markup from a fixture, and assert your empty-state rule fires. Treat the editor as an opaque JS widget that produces a string.

Given the maturity level, budget for reading source over docs. There's likely no docs site beyond the README. The component class is small; read it end-to-end before building features on top, and vendor-fork it ("repositories" + "type": "path") if you need a one-line JS tweak rather than waiting on a release.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource