tanmuhittin/nova-collapsible-groups
Laravel Nova package that organizes resource form fields into collapsible groups, turning long, complex forms into tidy sections users can expand and collapse. Configure group labels, default open or closed state, and styling with standard Nova fields—no custom components required.
This is a tiny asset-only package: it injects a JS/CSS bundle into Nova's layout that turns the sidebar group headings into collapsible toggles and (typically) remembers state in localStorage. No migrations, no config file, no publish step in the happy path.
composer require tanmuhittin/nova-collapsible-groups
Laravel package auto-discovery should register the provider (check extra.laravel.providers in the package's composer.json). If discovery is disabled in your app, add that provider class to config/app.php manually.
Where to look first, in this order:
vendor/tanmuhittin/nova-collapsible-groups/src/ — the service provider. You want to see the Nova::script(...) / Nova::style(...) calls; they tell you exactly which file is injected and under what name.resources/js/... or dist/js/...). Read the selector it queries and the localStorage key format — this is the whole package, and you need to know both.composer.json of the package for the nova / laravel version constraints.First use case: you already have public static $group = 'Reports'; on eight resources and the sidebar is a wall of headings. Install, hard-refresh, and the group headings become click targets that collapse their children and stay collapsed across navigations.
Verify with a hard reload and DevTools: Network tab should show the package asset loading with a 200, and the console should be clean. If nothing visually changes, the package is almost certainly not binding to your Nova version's DOM — see Gotchas.
Group + order resources, then let the package do the collapsing. The package doesn't own grouping; Nova's $group/$priority does. Keep grouping declarative and consistent:
class ReportResource extends Resource
{
public static $group = 'Reports';
public static $priority = 10; // order within the group
public static $sort = true; // alphabetical inside the group
}
The collapse unit is the group name, so treat $group strings as public API — they double as persistence keys in the browser.
Decide the default state deliberately. Most implementations of this pattern start expanded and only remember explicit user clicks. If you want a section collapsed for first-time users, the reliable route is forking the asset and seeding the default map rather than trying to pre-write localStorage keys from app code (the key format is an implementation detail you don't control).
Pair it with a documented sidebar convention. In app/Nova/NovaServiceProvider.php, or an App\Nova\Menu if you're on newer Nova, keep the ordered list of group names in one place so tooling/dashboards don't drift from $group values:
// Single source of truth used by resources and docs alike
final class NavigationGroups
{
public const REPORTS = 'Reports';
public const SETTINGS = 'Settings';
public const BILLING = 'Billing';
}
Then public static $group = NavigationGroups::REPORTS; — renaming a group becomes a one-line change instead of a grep across 40 resources, and you can consciously accept the one-time loss of saved collapse state.
Custom tools and links are a mixed bag. Only Resources/$group-driven entries are reliably grouped. Tools, cards, and Nova::resourcesLinks-style custom entries render in their own section and may or may not participate. Test the specific entry types you use rather than assuming uniformity.
On Nova 4+, skip this package entirely. Nova 4's native Nova::mainMenu() with MenuSection and MenuGroup gives you collapsible groups as a first-class, supported feature:
Nova::mainMenu(function (Request $request) {
return [
MenuSection::make('Operations', [
MenuGroup::make('Reports', [/* MenuItems */]),
MenuGroup::make('Settings', [/* MenuItems */]),
])->icon('cog'),
];
});
If you're on Nova 3, a 20–30 line resources/js/tool.js bound to the current sidebar DOM is less risk than depending on a 2019 asset bundle.
php artisan optimize:clear, republish Nova assets if you've ever customized public/vendor/nova, then hard-reload with DevTools "Disable cache" checked. Old bundles surviving in public/vendor are the usual culprit.Cannot read properties of null (reading 'querySelectorAll') or similar from the injected bundle. Resolve by controlling provider registration order, or fork.localStorage key is derived from the group name; rename $group and users get a stale entry plus a fresh expanded section. Additive on disk, confusing in behavior.$group = null renders as a plain link, so a "collapsible groups" demo can look broken if half your resources were never grouped.resources/js and register it with Nova::script() yourself. You keep the behavior, you own the fix when Nova bumps its DOM.<script> tag is in the rendered page source (not just the network panel), inspect localStorage for the expected keys after a click, and toggle the DOM node directly in Elements to prove the CSS half works independently of the JS half.authorizedToViewAny as usual.How can I help you explore Laravel packages today?