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

Widgets Laravel Package

filament/widgets

Filament Widgets provides the widget system for Filament panels, letting you build dashboards with stats, charts, tables, and custom widgets. Configure dashboard layouts, poll for updates, and reuse widget components across admin panels and resources.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check on the package name

filament/widgets is a read-only subtree split of the monorepo at filamentphp/filament. "Dependents: 0" is expected — nobody installs it directly. Day-to-day you get it transitively:

composer require filament/filament   # pulls filament/widgets, filament/panels, filament/support…
composer show filament/widgets       # confirm the version matches your other filament/* packages

Only add filament/widgets to composer.json explicitly if you are pinning individual Filament packages to trim the dependency graph — and then you must pin every filament/* package to the same version constraint, or you will get cross-major class-not-found errors. The repo you linked is the right place to read source and file issues; the docs you actually want live at filamentphp.com/docs/{3.x|4.x}/widgets.

Prerequisites for anything below: Laravel 10/11/12, Livewire 3, Tailwind compiled through Filament's own pipeline (you do not add widget paths to your tailwind.config.js unless you are overriding views).

First 10 minutes

  1. Scaffold the panel if you haven't: php artisan filament:install --panels, then create a user and log in at /admin.
  2. Look at app/Providers/Filament/AdminPanelProvider.php. Note ->widgets([...]) and ->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets'). Discovery is what you'll rely on 95% of the time — new classes in that folder appear on the dashboard with no registration step.
  3. Generate one of each and read the generated file:
php artisan make:filament-widget SalesOverview --stats-overview
php artisan make:filament-widget RevenueChart --chart
php artisan make:filament-widget LatestOrders --table
php artisan make:filament-widget Announcement --widget   # plain, Blade-only
  1. First real use case: replace a hardcoded stats row on your dashboard with a StatsOverviewWidget. That's the highest value-per-line change in the whole package — four numbers, four colours, a description and a sparkline in ~30 lines of PHP, no JS.
namespace App\Filament\Widgets;

use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;

class SalesOverview extends StatsOverviewWidget
{
    protected static ?string $pollingInterval = '30s';

    protected function getStats(): array
    {
        return [
            Stat::make('Open orders', number_format($open = Order::where('status', 'open')->count()))
                ->description('Awaiting fulfilment')
                ->descriptionIcon('heroicon-m-clock')
                ->color($open > 50 ? 'danger' : 'success')
                ->chart(Order::where('status', 'open')->pluck('count_7d')->all()),
            Stat::make('Revenue (30d)', '$' . number_format($revenue / 100, 2))
                ->description('+12% vs previous')
                ->descriptionIcon('heroicon-m-arrow-trending-up'),
        ];
    }
}
  1. For a plain widget, the class is just a Livewire component with a view:
class Announcement extends Widget
{
    protected static string $view = 'filament.widgets.announcement';
    protected int | string | array $columnSpan = 'full';
    protected static ?int $sort = -10;
}

The Blade file wraps itself in the package's own component — not x-filament::widget:

<x-filament-widgets::widget>
    <x-filament::section>
        {{ $this->message }}
    </x-filament::section>
</x-filament-widgets::widget>

Implementation Patterns

Choose the base class by data shape, not by ambition

Need Base class You implement
KPI cards with deltas/sparklines StatsOverviewWidget getStats(): array
One Chart.js chart ChartWidget getData(), optionally getType(), getOptions()
Paginated/filterable list of records TableWidget table(Table $table): Table
Anything else (feeds, iframes, markdown) Widget + Blade $view

AccountWidget and FilamentInfoWidget are the two shipped concretes; everything else in filament/widgets is infrastructure. Subclassing rather than composing is the intended extension point.

Dashboard composition

Three layers, pick the narrowest that works:

// 1. Whole panel (applies to every dashboard/user menu)
->widgets([AccountWidget::class, FilamentInfoWidget::class])

// 2. One dashboard page
class Dashboard extends Filament\Pages\Dashboard
{
    public function getWidgets(): array
    {
        return [
            ...parent::getWidgets(),
            SalesOverview::class,
            RevenueChart::make()->columns(2),   // WidgetConfiguration
        ];
    }

    public function getColumns(): int | array
    {
        return ['default' => 1, 'md' => 2, 'xl' => 4];
    }
}

// 3. Widget itself (static defaults, overridable per instance)
protected static ?int $sort = 20;
protected int | string | array $columnSpan = ['default' => 'full', 'xl' => 1];

->columns() on WidgetConfiguration is the per-instance override of getColumnSpan(); use it when the same widget appears in two layouts. Ties in $sort are ordered by discovery order — don't leave gaps, they will reorder unpredictably when you add files.

Filters: dashboard date range → widget query

Add HasFilters to your Dashboard page and a Filter form schema, then let each widget opt in:

use Filament\Widgets\Concerns\InteractsWithPageFilters;

class RevenueChart extends ChartWidget
{
    use InteractsWithPageFilters;

    protected static ?string $heading = 'Revenue';

    protected function getData(): array
    {
        $start = $this->filters['startDate'] ?? now()->subMonth();

        return [
            'datasets' => [[
                'label' => 'Revenue',
                'data' => Order::where('created_at', '>=', $start)
                    ->groupByMonth()->pluck('total')->all(),
                'backgroundColor' => '#0ea5e9',
            ]],
            'labels' => ['Jan', 'Feb', /* … */],
        ];
    }
}

This is the single most common interaction pattern in real apps: filters live on the page, widgets read them via the trait, no event bus needed.

Embedding outside a Filament panel

Widgets are Livewire components, so <livewire:app.filament.widgets.sales-overview /> works in any Blade view, including a plain controller-rendered page. It does require Filament's layout/asset pipeline for styling — in practice add the panel middleware stack (Filament\Http\Middleware\...) or at least the @filamentStyles/@filamentScripts directives, otherwise the fi-* classes render unstyled. Don't fight this; if you need widgets outside a panel, mount them in a Livewire page that uses Filament's layout.

Performance workhorses

  • Lazy loading is on by default for widget classes (protected static bool $isLazy = false; to opt out). Keep it on: the dashboard HTML flushes, then each widget makes its own request. It converts one slow TTFB into a fast one plus N small requests.
  • Polling replaces websockets for 90% of "live dashboard" asks: protected static ?string $pollingInterval = '30s'. Leave null unless a human is actually staring at it.
  • Cache the aggregate, not the widget. Cache::remember("stats.{$tenantId}", 300, fn () => ...) inside getStats()/getData() — the widget re-renders cheaply and polling stops hammering MySQL.
  • getData() must be JSON-serialisable and use sequential arrays for labels and each datasets.*.data. Associative arrays silently produce a broken chart with no exception.

Extending

Custom base widget for a house style:

abstract class BaseWidget extends Widget
{
    protected int | string | array $columnSpan = ['default' => 1, 'xl' => 2];

    public function getHeading(): ?string
    {
        return static::$heading ?? Str::headline(class_basename(static::class));
    }
}

Permissions belong in canView() — Filament honours it before rendering and before lazy-loading, so the query never runs:

public static function canView(): bool
{
    return auth()->user()->can('view_sales');
}

Gotchas and Tips

Overriding mount() on ChartWidget/TableWidget breaks things. Both base classes initialise state ($filters, table setup, chart id) in mount(). Always parent::mount() first, or you'll get a blank chart / "table is not configured" with no error.

getColumnSpan() wins over int $columnSpan. In newer versions the property is int | string | array; in older 3.x it was int | string. If you're publishing a widget across minor versions, define the method getColumnSpan() instead of the property — it survives both.

filament:cache-components / filament:optimize and widget discovery are enemies. After creating or renaming a widget class, a cached production build will still show the old list. Run php artisan filament:clear-cached-components (v3.2+) or filament:optimize-clear (v4). Same trap applies in CI: cache in the deploy step, clear in the build step.

Wrong view component = unstyled widget with zero errors. <x-filament::widget> renders an empty shell that swallows your slot content on some versions; the correct tag is <x-filament-widgets::widget>. If a custom widget renders nothing at all, check this first — it fails silently.

Polling intervals multiply. Ten widgets at '5s' is 120 Livewire requests/minute per user. Put pollingInterval only on the one or two widgets that need it, and stagger values ('10s', '30s', '60s') so they don't phase-lock.

Chart JS is bundled but your data isn't safe. Money as $1,234.56 inside ChartWidget data is fine as a label; as a dataset value it breaks the axis. Format in getOptions()/getData() with ->tooltipCallback-style options, keep raw numerics in data — or use Chart.js's ticks.callback in getOptions().

Stat::chart() needs a pre-computed array, not a query. It's passed straight through to a sparkline; it runs no aggregation. If you hand it 10 000 rows you get a 10 000-point SVG that freezes the browser. ->chart(collect($points)->take(-30)->values()->all()).

Livewire keys collide when you render the same widget twice. Two SalesOverview instances on one page share a Livewire id unless you pass a key: @livewire(SalesOverview::class, key('sales-overview-header')). Symptom is one widget updating twice and the other never.

InteractsWithPageFilters is silent outside a Dashboard. If the widget is dropped into a custom page without HasFilters, $this->filters is [] and every widget silently shows the default range. Guard with $this->filters['startDate'] ?? now()->subMonth() rather than assuming the key exists — that pattern also makes the widget reusable and testable.

Tailwind purge misses classes built at runtime. ->color($condition ? 'danger' : 'success') is fine (Stat maps those to compiled classes), but "text-{$colour}-500" in your own Blade is purged. Use Filament's semantic helpers (->color(), x-filament::badge) or safelist explicitly.

Testing a widget is just testing a Livewire component — no special harness needed, which is the strongest argument for the package:

it('shows open order count', function () {
    Order::factory()->count(3)->create(['status' => 'open']);

    livewire(SalesOverview::class)
        ->assertSuccessful()
        ->assertSee('Open orders')
        ->assertSee('3');
});

Add ->call('$refresh') between data mutations to assert polling behaviour, and assert canView() separately with actingAs() — permission bugs are otherwise invisible until a customer sees another tenant's revenue.

Debugging checklist when a widget vanishes (in order of frequency): not in a discovered path / wrong namespace in discoverWidgets() → component cache stale → canView() false → $view typo → exception swallowed inside getData() (check storage/logs/laravel.log; Livewire surfaces widget exceptions as a generic "component error" block in the UI). php artisan filament:about prints panel, discovery and cache state in one screen — run it before you go hunting.

Version drift across filament/* packages is the number-one upgrade failure mode with subtree-split installs. Before every upgrade: composer outdated 'filament/*' and bump them as a set. Mixing a v3 filament/widgets with a v4 filament/panels produces Call to undefined method inside Widget that looks like your bug but isn't.

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.
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics
robbiep/afterthedeadline
frankkessler/incontact-laravel-oauth2-rest
jlxh/laravel-userlog
davidmgilo/timetables
qylinfly/role-permission
reflect-digital/conduit
takashato/laravel-vietqr
aitor24/laralang
kraenzle-ritter/entity-relations
dindigital/din-image
pomirleanu/image-colors
sawmainek/laravel-api-generator
delatbabel/jsonresponses
mouhamedfd/permissions-generator
eduplus/qr