marshmallow/nova-tabs
Organize Laravel Nova resource forms into clean, navigable tabs. This package lets you group fields into tabbed panels, keeping complex forms tidy and improving the editing experience for admins and users alike.
Read this first: this package has no published documentation reachable from the repo listing, so treat it as a reference implementation rather than a fully documented dependency. Budget ~30 minutes in src/ before composer require. The v2.31.0 release (a merged community PR fixing PHP 8.4 deprecations) confirms two things the earlier assessment got wrong: the package is on a long 2.x release line (this bump is v2.30.0 → v2.31.0, not a first release), and it is not fully archived — an outside contributor (@stefvanesch) had a PR merged. It is still very low-traffic with no docs, so plan for reading source rather than searching for answers.
Prerequisites:
require block on laravel/nova in composer.json for the exact tag you install.Minimal steps:
composer require marshmallow-packages/nova-tabs:^2.31
Nova field packages usually ship a service provider auto-discovered via extra.laravel.providers. If it ships JS/CSS, run:
php artisan vendor:publish --tag=nova-tabs-assets
php artisan nova:assets
Where to look first, in this order:
composer.json — the require constraint on laravel/nova tells you instantly whether it will install against your Nova version. Also check whether the 2.x line has moved its minimum PHP beyond yours.src/ — identify the field class (usually NovaTabs, Tabs, or Tab). The fields() / addTab() / withToolbar() methods define the entire public API. There is no separate docs surface worth hunting for.resources/js/ — confirms whether it renders client-side (JS component) or is pure PHP layout sugar. This determines whether nova:assets matters.README.md — if present, it is the only usage reference; expect it to be a single code block.First use case — grouping a bloated fields() method into logical tabs on a Resource:
use Marshmallow\NovaTabs\NovaTabs;
public function fields(NovaRequest $request)
{
return [
NovaTabs::make('Details')
->addTab('General', [
ID::make()->sortable(),
Text::make('Name')->rules('required'),
])
->addTab('Content', [
Textarea::make('Body'),
Markdown::make('Notes'),
])
->addTab('SEO', [
Text::make('Meta Title'),
Text::make('Meta Description'),
]),
];
}
Verify the exact method names against src/ before committing — forks of tab packages have diverged (addTab vs tab vs ->tabs([...])), and a 2.x major bump may have changed them relative to older blog posts you find.
Tab-per-concern on large resources. The core value is keeping fields() readable and the form scannable. A workable heuristic: any resource with more than ~15 fields or more than two Panel groups is a tabs candidate. Group by audience (Editor / SEO / Publishing) rather than by column type — that is what actually reduces cognitive load for content editors.
Conditional tabs. Almost every tabs package exposes canSee() on the tab object, because tabs are fields under the hood. Use it to hide irrelevant tabs per-request:
NovaTabs::make('Details')
->addTab('Publishing', [...])->canSee(fn ($request) => $request->user()->can('publish'))
->addTab('Advanced', [...])->canSee(fn ($request) => $request->user()->isAdmin())
If the package does not support per-tab canSee, wrap the whole NovaTabs field instead.
Detail view reuse. Tabs are typically a field container, so the same instance can be dropped into fields() for both index-detail and forms. If the package renders a JS tab bar only on forms, guard detail usage or accept the fallback rendering.
Composing with native Nova layout. Don't nest NovaTabs inside Panel and then put Panel inside a tab — double chrome looks bad and some implementations break event handling. Pick one layout primitive per level: NovaTabs at the top, Panel/Collapsible inside tabs (if supported), never the reverse.
Insulating your resource from the package. Structure fields() so the tab container is the only package-coupled line:
return $this->tabbed([
'General' => [...],
'Content' => [...],
]);
protected function tabbed(array $groups): array
{
return NovaTabs::make('Details')
->tabs($groups) // adjust to actual API
->toArray();
}
Wrapping it in one helper means swapping to eminiarts/nova-tabs, kosatyi/nova-tabs, or native Nova field groups is a single-method change — and a 2.x → 3.x bump only touches that method.
Upgrade hygiene across the 2.x line. Since the package versions frequently at patch level (2.30 → 2.31), treat patch bumps as behavioural. Read the compare diff link in each release note (e.g. compare/v2.30.0...v2.31.0) before deploying; the 2.31.0 diff is deprecation-only, but that is not guaranteed for future patches.
Index columns are unaffected. Tabs are a form/detail concern; add sortable()/searchable() on the underlying fields as normal and keep fields()/fieldsForIndex separate if your resource splits them.
1.0.0 — that advice is outdated. The package is on 2.x. A caret constraint (^2.31) is reasonable now that the package is receiving at least dependency-level maintenance. Still commit composer.lock, and if you need guaranteed API stability for production, vendor-fork into packages/ with a path repository.canSee but a field inside it fails validation, Nova may report an error on a field the user cannot see. Keep required fields in the first tab, or ensure the failing tab is always visible.dependsOn across tab boundaries. Field dependency resolution is DOM-based in Nova; a field in Tab B depending on a field in Tab A may fail because the controller isn't mounted when the tab is lazily rendered. Keep dependent pairs in the same tab.php artisan nova:assets and hard-refresh. A stale public/vendor/nova-tabs directory manifests as "the field renders as a plain text input" — a classic Nova package symptom. This bites hardest right after a version bump.->withMeta() and a JS mounted() hook, it is sensitive to field array order. Keep NovaTabs as the sole entry in fields(), not appended after other fields.dump((new YourResource($model))->fields(NovaRequest::create('/'))) in a route or Tinker gives you the resolved field array — faster than guessing about withMeta keys.Laravel\Nova\Fields\Field subclass with a small component override. Adding ->withMeta(['tabs' => $groups]) and a thin Vue component is a ~50-line patch if you need to reimplement rather than adopt.eminiarts/nova-tabs (the long-standing option with real dependents), and kosatyi/nova-tabs. Unless you specifically need this package's API shape, adopting one of those avoids the documentation gap entirely.How can I help you explore Laravel packages today?