codenco-dev/nova-grid-system
A Laravel Nova package that adds a flexible grid system for arranging fields, cards, and content into responsive multi-column layouts. Configure column widths, spacing, and breakpoints to build cleaner, more organized Nova dashboards and resource screens.
Compatibility first, code second. This is a Nova 4-era tool with its last release on 2023-02-20 (v2.0.1). Before you write a single field, run a throwaway install on a sandbox branch against your exact Nova, Laravel, PHP, and Node versions. If you are on Nova 5, Laravel 11+, or PHP 8.2+, treat "does the compiled JS register with Nova's asset pipeline" as the real go/no-go gate — that is where Nova tools break first, and it looks like an API problem until you check the network tab.
Install and register:
composer require codenco-dev/nova-grid-system
Most Nova tools auto-register, but if the tool doesn't appear, add it explicitly:
// app/Providers/NovaServiceProvider.php
public function tools()
{
return [
new \CodencoDev\NovaGridSystem\NovaGridSystem,
];
}
Read these in this order before building anything:
README.md — the fluent span method(s) are the entire public API. Do not infer signatures from the marketing description; the docs are the contract.src/ — find the service provider and any macros registered on Nova's Field class. That macro list is your public surface.resources/js/components/ — the Vue wrapper is where column math lives. Confirm whether it emits CSS Grid (grid-column: span N) or Tailwind width classes. Every pitfall below flows from that one detail.config/nova-grid-system.php (if a publish tag exists) — usually breakpoints and gap. Publish and commit it early so config drift shows up as a diff rather than a mystery.First use case: one resource, one layout pass. Pick a resource with 6–10 flat fields and rebuild fields() as a 12-column layout — a full-width textarea on top, two 6-span fields, then a 4/4/4 row. If the release tag is missing dist/, run npm install && npm run production inside the package. Diff the form against stock Nova before and after. If the layout holds, you understand the package. If it doesn't, stop reading PHP and go look at the compiled assets.
Treat the grid as a layout annotation, not a structural wrapper. Keep fields() returning a flat array of real Nova fields and attach span metadata per field. Anything that reparents a field's Vue component risks breaking attribute resolution, which cascades into validation error mapping, dependsOn, fillUsing, and authorization. Nova's field contract is fragile — don't break it for layout convenience.
Extract layouts into their own class. A layout is just data, so it composes cleanly and can be reused inside Action::fields() and detail panels:
// app/Nova/Fields/ContactLayout.php
final class ContactLayout
{
public static function make(): array
{
return [
Text::make('Name', 'name')->gridSpan(6),
Email::make('Email', 'email')->gridSpan(6),
Textarea::make('Notes', 'notes')->gridSpan(12)->rows(4),
];
}
}
This pays off the moment a second resource needs the same shape, and it makes the exit path a one-file change. (Note: gridSpan is the assumed method name from the package's fluent API — verify against README.md before committing to it.)
Pick one breakpoint system and hard-code against it. Decide up front: stacked on mobile, 12 columns from tablet up. Then use spans of 3 / 4 / 6 / 12 everywhere. Mixing a percent-based value (50) copied from an example with a 12-column value (6) is how you end up reverse-engineering rounding at 11pm.
Grid inside panels, not around them. A grid of fields inside a Panel, Collapsable, or Tab renders fine. A grid containing panels is fragile — the panel's full-width wrapper tends to ignore the track. If you need a two-column page where each column is a panel, accept that panels span full width and arrange fields inside each one.
Array order is visual order. The grid doesn't sort, and it doesn't respect sortable(). Ordering and layout are orthogonal features — do not try to reconcile them.
Audit custom and third-party fields first. Any field whose Vue component expects to own its width will render oddly in a narrow cell. In-house fields, file uploaders, and BelongsTo search fields are the usual offenders. Drop each into a 4-span cell before committing to a layout that uses it.
Override styles at the app level, never in vendor/. Need tighter gaps or different breakpoints? Publish the CSS/JS and override via resources/css/nova.css or a small wrapper component. Editing vendor/ gets reverted on the next composer update and produces a "why did my layout change?" bug six weeks later.
Keep the dev loop tight. When iterating on a layout, run the package's watcher (or your app's) and keep the Nova page open in a second monitor. Layout tuning is a visual feedback loop — batching field changes and reloading between each one wastes the day.
It is effectively unmaintained. 81 stars, zero dependents, last release February 2023. Assume no one patches the issues you file. Mitigations: pin the exact version in composer.json, and if you use the grid in more than two or three resources, vendor it (a path repository or a fork) so you own the Vue component and can patch breakage after a Nova upgrade.
Asset compilation is the #1 failure mode. Nova tools ship precompiled JS. If the layout simply does nothing — fields render normally, no column behavior — check in this order before debugging PHP: (1) does dist/js/tool.js exist and is it non-empty under vendor/codenco-dev/nova-grid-system/? (2) does Nova's admin network tab show the tool's JS loading? (3) npm install && npm run production inside the package and re-check. Nine times out of ten it's a missing or stale dist/ from the release tag.
Dynamic Tailwind classes get purged. If the wrapper builds class strings like `col-span-${span}`, production CSS won't contain them unless they're safelisted. Symptom: works under npm run dev in the package, silently collapses under npm run production. Fix in the package's tailwind.config.js safelist and rebuild — or migrate the wrapper to inline style="grid-column: span N / 12", which sidesteps Tailwind entirely.
Nova's own sizing methods fight the grid. fullWidth(), size('w-1/2'), stacked(), and the grid's span method all set width-ish classes on different layers. Stacking them produces unpredictable widths. Pick one per field. If a field visibly ignores its span, the first fix is removing its Nova sizing method.
Commit dist/ in your fork. Nova doesn't build tool assets for you. A fork whose dist/ is gitignored will look broken on the next deploy with no obvious error. Commit it, or add a build step to CI.
Debug in the browser, not in PHP. Open Nova's devtools, inspect the field wrapper, and read the computed grid styles. Whether the span is applied — and whether it takes effect — is visible in the Elements panel in seconds. Chasing it through PHP traits and macros wastes an hour on a CSS problem.
dependsOn and conditional visibility still apply. The grid changes how fields are laid out; it does not change when they render. When a dependsOn field is hidden, its grid cell collapses and the row reflows. That's usually what you want, but verify against your design — you may need explicit spans on every sibling to keep rows stable.
Plan an exit path. Because this is layout sugar over Nova's field system, removal is cheap: strip the span calls, keep fields() flat, and the resource reverts to stock Nova. Keep layout classes isolated under app/Nova/Fields/ so a fork, a rewrite, or a removal is a one-file change, not a package-wide refactor.
How can I help you explore Laravel packages today?