asmit/filament-mention
asmit/filament-mention brings @mentions to Filament RichEditor fields. It provides searchable, model-backed mention suggestions, customizable item rendering, and straightforward form integration, making it easy to tag users or other mentionable records directly within rich text.
Install the package and publish its config so you have something to edit rather than a vendor file:
composer require asmit/filament-mention
php artisan vendor:publish --tag="filament-mention-config"
Register the plugin on the panel (this is the path most installations use, since the mention extension has to be injected into Filament's RichEditor asset bundle globally):
use Asmit\FilamentMention\FilamentMentionPlugin;
public function panel(Panel $panel): Panel
{
return $panel
->plugins([
FilamentMentionPlugin::make(),
]);
}
Then open config/filament-mention.php first — that is the single source of truth for what is mentionable, how the dropdown searches, and how many results are returned. A typical shape is:
return [
'model' => App\Models\User::class,
'search_column' => 'name', // or 'username'
'limit' => 5,
'trigger' => '@',
];
If your version of the package ships a field-level macro instead of (or in addition to) a panel plugin, you will see it documented as RichEditor::make('body')->mentionable() — check the installed version's README before wiring the panel provider, because the registration style changed between early releases.
First use case: a comment/ticket body field. Add a RichEditor::make('body') to a form, confirm typing @ opens the suggestion dropdown with records from the configured model, and that selecting one inserts the handle into the editor state. Once that round-trips through ->save(), you have the 90% case working.
Keep mentionables out of the global config when you have more than one editor. Config is a singleton; if your Ticket editor should mention only staff and your Comment editor should mention everyone, define the model/search via the plugin or field instance rather than the published config, or build a small factory that returns a configured RichEditor per context.
Scope the suggestion query for multi-tenancy. The package resolves candidates from the configured Eloquent model. If you run a tenant-scoped panel, wrap the model resolution in your own query scope (a mentionable() static on the model, or a custom resolver if the package exposes one) so a user in tenant A cannot enumerate tenant B's users by typing @a.
Store both the display string and the resolved IDs. The editor inserts @Jane Doe as text. That text is not a stable reference — people rename, duplicate names exist, and handles with spaces break naive regex parsing. The robust pattern is to parse mentions on save and persist them to a mentions morph table:
// App\Models\Post
public function mentions(): MorphMany
{
return $this->morphMany(Mention::class, 'mentionable');
}
// in the saving hook
$post->mentions()->delete();
preg_match_all('/@([\p{L}\p{N}_ .-]+)/u', strip_tags($post->body), $m);
$ids = User::whereIn('name', $m[1])->pluck('id');
$post->mentions()->createMany($ids->map(fn ($id) => ['user_id' => $id]));
Then notifications/@-badges read from the pivot table and never re-parse HTML.
Render through a dedicated presenter, not raw HTML. Output the editor HTML with Filament\Support\helpers\html-style sanitization or a RichContentRenderer, and post-process mention spans into profile links server-side. Keeping linkification in one presenter means the styling of a mention is a single place to change.
Reuse across panels by extracting a form schema method. If two panels both need a mentionable editor, put the RichEditor definition in a static TicketForm::schema() and call it from both, so the extension config cannot drift.
Testing: Livewire tests exercise the form state, not the JS dropdown — set the field value directly (fillForm(['body' => 'Hi @Jane'])) and assert on the persisted mentions. The TipTap dropdown itself needs a browser test (Dusk/Playwright); do not try to assert it from Pest.
composer update, run php artisan filament:assets (and php artisan optimize:clear) or you will silently keep the old bundle and wonder why config changes do nothing.config:cache in production means the published config is frozen; changing model on the server without re-caching looks like the package ignoring you.@admin by hand without ever selecting from the dropdown, so never grant permissions from parsed mentions without verifying the target exists and is visible to the author.mews/purifier) before {!! !!}. This is a general Filament issue but mention output is a common injection point because people assume the dropdown constrained input.@Jane Doe truncates to @Jane. Prefer a unique handle (username) as the inserted value and keep the display name for rendering.composer.json and budget a spike before upgrading the panel.view-source for whether the JS asset is actually loaded → php artisan filament:assets → then the package source in vendor/asmit/filament-mention/resources/js. The package is small enough (44 stars, single maintainer) that reading the extension source is faster than waiting for docs.RichEditor::configureUsing() for applying the extension panel-wide without touching every form. If you need behaviour the package does not expose, write your own TipTap Mention extension and register it as an asset — you are not locked in, but you will be maintaining it.How can I help you explore Laravel packages today?