Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Custom Dashboard Card Laravel Package

beyondcode/nova-custom-dashboard-card

Easily add custom dashboard cards to Laravel Nova. Render your own Blade views or Vue components as cards, giving you full control over the metrics, charts, and content shown on your Nova dashboards.

View on GitHub
Deep Wiki
Context7

Getting Started

Install, then read three files before you write any code.

composer require beyondcode/nova-custom-dashboard-card

The package is one PHP card class plus a Vue render layer, archived since August 2018. The vendor directory is the documentation. Open, in this order:

  1. src/CustomDashboardCard.php — the entire API. It extends Nova's Card, defaults to full width, and exposes ->component('Name') for the Vue component plus a fluent setter for the payload you hand to the front end. Note the exact method name (withData() in the examples below) from the source, not from blog posts.
  2. resources/js/components/ — the wrapper that does <component :is="card.component" />. This tells you exactly what shape your own component receives: a single card prop.
  3. The service provider — confirms how the compiled asset is registered via Nova::script() and whether anything is publishable.

The package also ships an Artisan generator for new cards. Find its real signature in the provider's command registration rather than guessing the name; on an 87-star 2018 package, the command is undocumented and may not match what you expect.

First use case, ~10 minutes — prove the pipeline with a throwaway card.

PHP, in app/Providers/NovaServiceProvider.php:

public function cards()
{
    return [
        CustomDashboardCard::make()
            ->component('WelcomeCard')
            ->withData(['name' => auth()->user()->name, 'count' => User::count()]),
    ];
}

Vue, resources/js/components/WelcomeCard.vue:

<template>
  <div class="p-6">
    <h2 class="text-lg font-bold">Hi {{ card.name }}</h2>
    <p class="text-3xl">{{ card.count }} users</p>
  </div>
</template>

<script>
export default { props: ['card'] }
</script>

Register it globally, in resources/js/nova.js:

Nova.booting((Vue) => {
  Vue.component('welcome-card', require('./components/WelcomeCard.vue').default)
})

Then npm run dev and reload /nova. If you get a blank tile with a clean console, you've hit the component-name mismatch described below — which is the fastest possible way to learn the one thing that breaks most often.

Version check before you commit: this is Vue 2 / Nova 1.x–2.x. On Nova 4+ you already have Nova::dashboards(). In that case read the source for the pattern and rebuild it as a first-party card.

Implementation Patterns

Compose, don't decorate. The highest-value day-to-day pattern is a full-width wrapper that hosts existing metric cards as children, turning a flat wall of tiles into labelled panels:

CustomDashboardCard::make()
    ->component('PanelCard')
    ->withData([
        'title' => 'Support',
        'cards' => [new OpenTickets, new SlaBreaches, (new ResponseTime)->width('1/2')],
    ])
    ->width('full'),

The wrapper renders each child as a real Nova card (<component :is="child.component" :card="child" />), so each metric keeps its own data fetching and its own width(). Keep the wrapper full; let the children sum to 12.

Do the work in PHP, pass scalars to Vue. cards() runs once per dashboard load, so compute there and keep the component dumb:

->withData([
    'revenue' => Cache::remember('dash.revenue', 300, fn () => Order::sum('total')),
    'churn'   => Cache::remember('dash.churn', 300, fn () => $this->churnRate()),
])

Config-driven dashboards. Component name is a string and payload is an array, so the whole layout can come from config or a per-role map:

return collect(config("dashboards.{$user->role}"))
    ->map(fn ($classes, $title) => CustomDashboardCard::make()
        ->component('PanelCard')
        ->withData(['title' => $title, 'cards' => array_map(fn ($c) => new $c, $classes)]))
    ->all();

Adding a metric becomes a config line rather than a deploy. That is the real payoff of the pattern, even if you never install the package.

Persona dashboards by swapping the component. Same card class, different Vue component per audience, gated with Nova's own canSee():

CustomDashboardCard::make()
    ->component('ExecOverview')
    ->canSee(fn ($request) => $request->user()->hasRole('exec')),

Day-to-day loop. npm run watch in the app root, keep the component registered once in Nova.booting, and inspect the card prop in Vue DevTools — it is the single source of truth for everything PHP sent. Anything you don't declare in props still arrives on card, so props: ['card'] is almost always enough.

Embedding external tools. For a BI iframe, the component is a five-line template reading card.src. Put the URL in .env, never in the payload.

Smoke tests worth writing: card registers, asset loads, component resolves, canSee() actually hides it. Four assertions that catch every failure mode this package has.

Gotchas and Tips

A blank tile is always the same cause. ->component('MyCard') must resolve to a globally registered Vue component whose tag matches. Register Vue.component('my-card', ...) and call ->component('my-card'). Mismatches render nothing and log nothing useful — check the browser console and the Vue component tree before you touch any PHP.

Case sensitivity bites in string templates. Kebab-case everywhere. A PascalCase tag inside a runtime-compiled template can silently fail even when the component is registered.

Never hand-render child metrics. If you write your own markup for nested metrics instead of passing real Nova card instances through, you get empty tiles — each metric fetches its own data client-side via its own XHR. Blank nested tile? Open the network tab. No request at all means the wrapper isn't mounting the real component.

Widths must sum to 12. Children keep their own width(). A wrapper that isn't full plus children that assume full width produces unpredictable wrapping. Set the wrapper to full and audit the children.

Everything in cards() runs on every dashboard view. Compute a total inline and you've added latency to the admin home page for every user, every load. Cache aggressively, or move the work into a JSON endpoint the Vue component calls.

Serialization leaks. Anything passed to the payload becomes JSON in the HTML. $hidden is respected by toArray(), but $visible, appended accessors, and loaded relationships often aren't what you expect. Pass scalars or explicit ->only([...]).

canSee() doesn't cascade. Hiding the wrapper hides the panel, but inner cards' own canSee() is not re-evaluated because you built the child array yourself in PHP. Authorize at the point you build that array, and scope every query by tenant.

Vue 2 only. The 2018 webpack build will not rebuild under current Node/Vite without work. If you fork, budget the JS toolchain as the expensive half, not the card class.

It will never be patched. No security response, no PHP 8 fix, no Nova compatibility release. Pin the version in composer.json, keep a vendored copy as a rollback artifact, put it on the platform upgrade checklist, and name an owner.

Extension points, in order of safety: subclass CustomDashboardCard to override the component or add fluent setters → write the Vue component → only then edit package source, and vendor it first. The class is small enough that subclassing costs nothing.

Know when to stop. This is a one-off screen tool, not a platform capability. The moment a second team wants a custom dashboard, promote the pattern into a shared internal card library with common conventions (audience grouping, width rules, canSee() gating) instead of letting each team fork its own archived copy.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource