marshmallow/nova-tags-field
A Laravel Nova tags field for attaching multiple tags to resources, with a searchable dropdown and tag management in the Nova admin panel. Simplifies many-to-many tagging for models like posts or projects while keeping editing fast and intuitive.
The package is a single Nova field that maps one database column to a comma-ish free-text tag input. There is no migration, no pivot table, and no model trait — the entire footprint is a field class plus a compiled Vue component.
Install:
composer require marshmallow/nova-tags-field
(Verify the exact constraint name — the repo is marshmallow-packages/nova-tags-field, and Composer names in small packages often differ from the GitHub slug. Run composer show marshmallow/* after install to confirm.)
Add the column yourself:
Schema::table('posts', function (Blueprint $table) {
$table->string('tags')->nullable()->after('body');
});
Register the field on the resource:
use Marshmallow\NovaTagsField\Tags;
public function fields(NovaRequest $request)
{
return [
Tags::make('Tags')
->rules('nullable', 'string', 'max:255'),
];
}
Where to look first. Read the field class in vendor/marshmallow/nova-tags-field/src/ — that file tells you the real namespace, the default separator/format, and any fluent options (separator, limit, placeholder). Then check dist/js/ for the compiled component; because the repo is archived you cannot recompile it, so if dist is missing the field simply will not render.
First use case. Labels on a CMS resource — Post, Article, Product — where you want quick visual categorisation in the index view and a free-text input in the form. Anything more relational (tag pages, tag clouds, tag counts) is outside this package's brief.
Normalise at the model boundary, not in Nova. The field stores whatever the user typed. Add an accessor/mutator or a cast so every other consumer of the data sees a clean array:
public function setTagsAttribute($value): void
{
$tags = collect(explode(',', (string) $value))
->map(fn ($t) => trim($t))
->filter()
->unique()
->implode(', ');
$this->attributes['tags'] = $tags ?: null;
}
public function getTagListAttribute(): array
{
return array_filter(array_map('trim', explode(',', (string) $this->tags)));
}
Index display. Raw comma strings are noisy in the table view. Either truncate with ->displayUsing(fn ($value) => Str::limit($value, 40)) or use fieldsForIndex / a Badge field driven by tagList to render chips. The latter is usually what stakeholders actually expect to see.
Search. ->searchable() on the field will not work out of the box for a plain column; you need Nova::resource searchable columns or an explicit searchableColumns(['tags' => 'like']). Add a where('tags', 'like', "%{$q}%") to your resource's buildIndexQuery override if you want tag-aware filtering from the search bar.
Filtering. Write a small custom Nova filter rather than fighting the field:
public function apply(Request $request, $query, $value)
{
return $query->where('tags', 'like', "%{$value}%");
}
For "has any of these tags" filters, explode the input and orWhere each term.
Feed suggestions from live data. If the field supports a datalist/options argument, populate it from DB::table('posts')->whereNotNull('tags')->pluck('tags') and split/flatten. This keeps tag vocabulary from drifting without introducing a real taxonomy.
Keep a graceful exit. Put the field behind a small helper or a resource-level fields() conditional so that when you eventually outgrow it (see below) you can swap to BelongsToMany + spatie/laravel-tags without touching every resource.
composer.json, and check that the compiled dist assets exist and are compatible with your Nova major — Nova 4/5 moved to Vue 3 and Inertia, and old Nova 2/3 components will silently fail to render.UPDATE posts SET tags = REPLACE(tags, 'old', 'new'). There is no join, so "list all distinct tags" is SELECT DISTINCT plus string splitting on the PHP side, which does not scale.explode(',') corrupts tags. Either pick an exotic delimiter (|) and enforce it in validation with a regex, or store JSON in the column — but then you must also override resolve/fill so the field receives and emits the format it expects.LIKE '%car%' matches cartoon, scar, carousel. Use FIND_IN_SET on normalised comma strings, or accept the fuzziness consciously.PHP and php become indistinguishable in queries but remain distinct strings in the column. Lowercase on write if you ever group by tag."" or a trailing comma. The mutator above handles it; without one you get ",,," in the database and blank chips in the UI.string(255) is about 30–40 tags. If you expect more, use text and drop the max:255 rule, or the input will silently truncate on save.->rules('nullable', 'string', 'max:255', 'regex:/^[a-z0-9 ,\-]*$/i') to keep the delimiter clean.resolve() / fillUsing(), and pass ->component('your-custom-tags') if you need to fork the Vue side. Since you cannot rebuild the archived assets, a fork means writing your own component anyway — at which point evaluate whether the package earns its place at all.How can I help you explore Laravel packages today?