sietse85/nova-button
Add customizable action buttons to Laravel Nova resources with sietse85/nova-button. Trigger links, modal forms, or custom logic directly from resource pages, dashboards, and toolbars using simple, flexible configuration.
Install and verify scope
composer require sietse85/nova-button
There is no migration, no config file to publish, and no manual service provider registration — Nova's package discovery handles it. The whole install is one composer line. Pin the exact tag in composer.json (for example "sietse85/nova-button": "1.0.3") rather than floating ^1.0. You want the Nova version bump and the button version bump to land in the same commit as a deliberate pair, because the package's Vue component renders inside Nova's internal component contract.
Where to look first, in this order
vendor/sietse85/nova-button/src/ — this is a small package (one field class plus a thin helper). Read it before you write a line of usage. It is more authoritative than the README, and the fluent method names can drift between the README and the tag you actually installed.vendor/sietse85/nova-button/dist/ — this is the compiled Vue bundle that renders the button in your authenticated admin UI. Understand what it is and whether you can rebuild it from source. That is a supply-chain question, not a curiosity.First use case: pick a read-only one
Do not make your first button destructive. Start with something trivially safe on a detail view — "Open in Stripe", "View invoice PDF", "Copy API key" — to prove rendering, URL resolution, and the click path.
use Sietse85\NovaButton\Button;
public function fields(NovaRequest $request): array
{
return [
// ... your normal fields
Button::make('Open in Stripe')
->url(fn () => "https://dashboard.stripe.com/customers/{$this->stripe_id}")
->icon('external-link')
->onlyOnDetail(),
];
}
A Nova-fluent developer gets a rendered button in under a day. The real budget is authorization, idempotency, and the audit trail behind the button, not the button itself.
Wrap the vendor class on day one
Before the second call site, create an internal field class:
namespace App\Nova\Fields;
use Sietse85\NovaButton\Button as BaseButton;
class ActionButton extends BaseButton
{
// Re-expose only the fluent API you actually use.
}
This is the highest-leverage thing you will do with this dependency. If the package is abandoned or a Nova upgrade breaks the component, the rewrite is one file instead of a grep across every resource.
Decide URL vs. Nova Action per button, not per resource.
The anti-pattern to refuse: a growing fleet of ad-hoc POST /admin/users/{user}/do-thing routes, each with its own ad-hoc auth check, because "it was quicker than an Action." That is how a view-layer convenience becomes an unaudited mutation surface.
Write confirmation copy as consequences, not questions.
ActionButton::make('Force resync')
->confirm('This re-pulls all data from the ERP and overwrites local records. Continue?')
->classes('bg-red-600 text-white')
->canSee(fn (NovaRequest $request) => $request->user()->can('resync', $this->resource));
"Continue?" conveys nothing. "This overwrites local records" is information the operator can act on. Ops teams iterate on this copy after every misfire, and it lives in PHP next to the fields — so tightening it is a PR, not a design cycle.
Gate per-row visibility with canSee.
ActionButton::make('Approve')
->onlyOnDetail()
->canSee(fn ($request) => $this->resource->status === 'pending_review'
&& $request->user()->can('approve', $this->resource));
canSee is your hook for both state-gating and policy-gating, and it keeps operators from clicking buttons that would fail anyway. But be precise about what it is: UX hygiene, not a security control. The route or Action behind it must enforce the identical policy independently.
Centralize the catalogue once you have more than three.
trait HasOperatorButtons
{
protected function operatorButtons(NovaRequest $request): array
{
return [
$this->resendInviteButton($request),
$this->impersonateButton($request),
];
}
}
Merge this into fields(). You get one file to review on a Nova upgrade, one file to hand a security reviewer, and one place that lists every mutation affordance in the admin panel.
Queue anything that takes more than a few seconds.
A button firing a synchronous 30-second ERP sync will time out, and an impatient operator clicks it three more times. Route it through a queued Nova Action or a job and surface queued/running/complete state. If the button UI has no progress affordance, at minimum say "this runs in the background" in the confirmation copy.
Make non-idempotent endpoints idempotent on the server.
Confirmation dialogs do not stop double-clicks, and neither does a loading spinner on a page that has already been re-rendered. Guard the backend: a status transition check, a firstOrCreate keyed on a job id, a lock, a unique index. This is the classic failure mode for this class of button and it is entirely a server-side responsibility.
Use index placement sparingly.
Buttons on index views render per row. On a 50-row page that is 50 extra interactive elements — real DOM weight plus visual noise. Reserve index placement for a single high-frequency read-only affordance, and prefer onlyOnDetail() for anything mutating. Index placement also forces you to think about N+1: any canSee closure that dereferences a relationship needs that relationship eager-loaded in indexQuery.
Test the field, and test the endpoint separately.
fields(), assert the button field is present and its visibility closure returns the expected value for a given model/user. This catches the most common regression — a policy rename that silently hides or exposes a button.Failures here are silent. The characteristic Nova-upgrade breakage is a blank table cell or a browser console error, not a 500. Server-side monitoring will never catch it. Put an explicit checklist item on every Nova upgrade: open one index page and one detail page in staging, click one button, check the browser console.
Version-skew Nova and this package as a pair. "Nova 4+" is a floor, not a tested forward-compatibility promise. Nova's internal Vue component API is not a public stability contract, so upgrade both in the same commit and keep the pin exact.
Verify the compiled assets. dist/ is prebuilt JavaScript going directly into your highest-privilege authenticated UI. Either rebuild from source in CI and diff the output, or have someone read the bundle. This is the step everyone skips and it is a supply-chain decision, not a convenience one. If it won't reproduce, vendor a reviewed build.
canSee, confirm(), and disabled states are all client-side. Every endpoint must enforce Nova's guard, CSRF, and your policies independently. Assume a determined user can POST the URL directly, because they can.
A 403 or 404 behind a button is invisible to the operator. The most common production incident for this package is a button whose backing route was renamed and now fails with no visible feedback. Log the click server-side, surface failures in the UI where possible, and keep a documented button → endpoint mapping so the on-call engineer can trace it.
Watch N+1 on canSee closures in index views. A closure that lazy-loads a relationship turns a 50-row page into 50 queries. Eager-load in indexQuery or restrict the button to detail views.
Don't use this for bulk. Nova Actions cover multi-record and queued operations. Bending a button into a batch workflow is exactly the drift that accrues authorization and idempotency debt. Write the boundary into your team's conventions doc and point at it during reviews.
Don't use this for multi-step flows. Anything with a form, a wizard, or conditional branching is a Nova Tool. That is the honest answer, not a compromise.
Route labels and confirmation strings through __() from day one. They are plain PHP strings next to the fields — trivial to wrap now, miserable to retrofit into a hundred inline literals later.
Check keyboard focus and screen-reader labels on the rendered button during your first spike. If the shipped component doesn't handle accessibility, your internal wrapper class is the place to fix it once.
Watch the vendor, but size the bet correctly. Single maintainer, small user base, last release Feb 2025. Note that Nova is commercial and not distributed via Packagist, so dependent counts structurally under-report adoption for any Nova add-on — 0 here is far less alarming than for a general Laravel package. Mitigations stack neatly: MIT means forking is always legal, the wrapper class means the swap is one file, and the surface is small enough that a self-maintained patch set is realistic. Name one engineer as owner or it becomes orphaned infrastructure inside your own org.
Check method names against vendor/sietse85/nova-button/src/, not memory or the README. The fluent API is small and the docs may lag or lead your pinned tag. A minute in the source saves an hour of BadMethodCallException.
How can I help you explore Laravel packages today?