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

Spatie Laravel Tags Plugin Laravel Package

filament/spatie-laravel-tags-plugin

Filament form component for Spatie Laravel Tags: a tags input that attaches, edits, and groups model tags via an optional type(), supporting the original tags input's customizations. Note that leaving type() unset matches and attaches tags across every type in your database.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Match the plugin major to your Filament major. The ^5.0 constraint in the README targets Filament v5; use ^3.0 / ^4.0 for older panels. The -W flag is there because this is a subtree split that shares dependency constraints with the Filament monorepo — without it Composer will refuse to move Filament's locked version.

    composer require filament/spatie-laravel-tags-plugin:"^5.0" -W
    
  2. Publish and run Spatie's migrations (you get both tags and taggables):

    php artisan vendor:publish --provider="Spatie\Tags\TagsServiceProvider" --tag="tags-migrations"
    php artisan migrate
    
  3. Prepare the model with Spatie's trait. The Filament components call tagsWithType() / syncTagsWithType() directly, so the tags() morphToMany from HasTags must exist:

    use Spatie\Tags\HasTags;
    
    class Post extends Model
    {
        use HasTags;
    }
    
  4. There is no panel plugin class to register. Unlike most Filament plugins there is no ->plugin() call — you just import Filament\Forms\Components\SpatieTagsInput and Filament\Tables\Columns\SpatieTagsColumn wherever you need them.

  5. First use case to implement — a PostResource with two tag axes: a curated categories vocabulary and free-form topics:

    SpatieTagsInput::make('tags')->type('categories');
    SpatieTagsInput::make('topics')->type('topics');
    
    SpatieTagsColumn::make('tags')->type('categories');
    

Where to look first: the base TagsInput docs for styling/splitting/reordering behaviour (the Spatie field inherits all of it), then Spatie's docs on types and syncTagsWithType() — that method is what actually runs on save, and understanding it explains most of the surprises below.

Implementation Patterns

Treat types as separate vocabularies, not one big pool. Scoping the input and the column with ->type() is the whole point of the plugin; an unscoped tags input mixes categories and topics and becomes unusable fast.

SpatieTagsInput::make('tags')
    ->type('categories')
    ->splitKeys(['Tab', ','])
    ->reorderable();

SpatieTagsColumn::make('tags')
    ->type('categories')
    ->limitList(3);          // if your Filament version's TextColumn supports it

Multiple types on one record. Each field needs its own form-state key, while both still write through the tags() relationship with a different type(). Use distinct field names (tags, topics) and verify save behaviour on your Filament version — the load/save hooks are type-driven, not name-driven, but the two fields must not share a state key.

Constrain the vocabulary without giving up the free-text feel. suggestions() improves UX but does not stop users creating new tags, so pair it with a server-side rule:

SpatieTagsInput::make('tags')
    ->type('categories')
    ->suggestions(fn (): array => Tag::getWithType('categories')->pluck('name')->all())
    ->rules([
        fn (): Closure => function (string $attribute, $value, Closure $fail) {
            $invalid = collect($value)->diff(Tag::getWithType('categories')->pluck('name'));

            if ($invalid->isNotEmpty()) {
                $fail('Unknown category: '.$invalid->implode(', '));
            }
        },
    ]);

Table filtering. There is no built-in tag filter; wire one with Spatie's withAnyTags() scope:

SelectFilter::make('tags')
    ->label('Category')
    ->multiple()
    ->options(fn (): array => Tag::getWithType('categories')->pluck('name', 'id')->all())
    ->query(fn (Builder $query, array $data): Builder => $query->when(
        filled($data['values']),
        fn (Builder $query) => $query->withAnyTags(
            Tag::whereIn('id', $data['values'])->pluck('name')->all(),
            'categories',
        ),
    ));

Bulk tagging from the table — the input degrades gracefully with no record, so it works inside a BulkAction form:

BulkAction::make('tag')
    ->form([SpatieTagsInput::make('tags')->type('topics')])
    ->action(fn (Collection $records, array $data) => $records->each(
        fn (Post $post) => $post->attachTags($data['tags'] ?? [], 'topics'),
    ))
    ->deselectRecordsAfterCompletion();

Curate tags in a small admin resource. Ad-hoc tag creation in forms drifts (typos, casing, duplicates). A simple TagResource listing Tag::query()->where('type', 'categories') with rename/delete actions keeps the vocabulary clean — renames are safe because taggables reference the tag ID, not the name.

Seed/import deterministically. For fixtures and imports use Spatie's API rather than the form component:

Tag::findOrCreate('Laravel', 'topics');
$post->syncTagsWithType(['Laravel', 'Livewire'], 'topics');

Eager load in tables to avoid an N+1 for every row's badges:

->modifyQueryUsing(fn (Builder $query) => $query->with('tags'))

Gotchas and Tips

  • Forgetting the migrations is the #1 first-run error. The plugin ships nothing schema-wise; without tags + taggables you get "no such table" on the first form save.
  • The field name is not a relationship name. The component hard-codes tagsWithType() / syncTagsWithType() from HasTags. If you renamed your relationship or use a custom pivot, the plugin will not see it — write your own load/save hooks instead.
  • Save is a sync, not an attach. Submitting the form replaces all tags of that type. If an observer or job added a tag earlier in the same request, it is wiped. Do tag mutations in afterSave() / a queued job, or outside the form flow.
  • An empty tags field deletes every tag of that type. Usually correct, but dangerous for partial-edit forms or role-restricted fields — guard with ->visible() and ->dehydrated(false) so the hook doesn't run.
  • Never call ->searchable() or ->sortable() on SpatieTagsColumn. There is no tags column to sort/filter on; you'll get SQL errors. Provide ->searchable(query: fn ($query, $search) => $query->withAnyTags([$search], 'topics')) or a whereHas('tags', ...) scope instead.
  • Column type must match the input type. A topics column against a categories field renders empty with no error.
  • Typo'd types fail silently. 'categorie' vs 'categories' creates a parallel namespace that looks fine in the form. Wrap types in a PHP enum or constants and pass ->type(TagType::Category->value).
  • Translations bite pluck('name'). With Spatie's translatable names, name is a JSON column; reads return the current locale, and SQL sorting/filtering by name is unreliable. Sort in PHP or filter by ID (pluck('name', 'id')).
  • No FK cascade on taggables. Deleting a model leaves pivot rows behind. Add a deleting observer that detaches, or a pruning command.
  • Multi-tenancy is not handled. Tags are global; per-tenant vocabularies require a team_id on tags, a custom Tag model via config('tags.tag_model'), and scoping every query — including the ones you write in filters and suggestions.
  • Tag creation is a privilege. The type is a namespace, but anyone who can submit the form can mint new entries in it. Restrict the field to trusted roles or validate against a curated list (pattern above) — free-text tags are a taxonomy decision, not a UI detail.
  • Bulk-action usage depends on a null-record guard. It works today because the component checks for a ?Model; if you fork or upgrade, re-test the "no record" path.
  • This repo is a read-only subtree split with very little standalone adoption. Issues and PRs belong in filamentphp/filament; pin versions, read the diff on upgrade, and assume you'll own the filters, infolist entry (use a TextEntry with ->badge()->getStateUsing(fn ($record) => $record->tagsWithType('topics')->pluck('name'))), and admin UI yourself.
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.
vento/laravel-airbrake
wfeller/laravel-batch
maco-studios/mage-os-installer
busarm/armie
siberfx/laravel-logger
kfteam1991/dcat-admin
dashed/dashed-laposta
dashed/dashed-ecommerce-montaportal
dashed/dashed-ecommerce-etsy
neohq/string-blade-compiler
log1x/acf-composer
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager