| Install | |
|---|---|
composer require mddev31/filament-dynamic-dashboard |
|
| Latest Version: | v0.4.3 |
| PHP: | ^8.3 |
User-configurable dashboards for Filament v4+.
Filament Dynamic Dashboard lets end-users create, switch, and manage multiple dashboards directly from the Filament UI. Widgets are added, removed, and reordered per dashboard without any code changes. Each dashboard supports its own filters, default values, and per-filter visibility settings. Optional Spatie Permission integration provides role-based dashboard visibility out of the box.

spatie/laravel-permission for role-based visibilityInstall via Composer:
composer require mddev31/filament-dynamic-dashboard
Publish and run the migrations:
php artisan vendor:publish --tag=filament-dynamic-dashboard-migrations
php artisan migrate
This package uses Tailwind CSS classes in its Blade views. For these styles to be compiled correctly, Tailwind must be able to scan the package views.
In your Filament theme file (e.g. resources/css/filament/admin/theme.css), add the following @source directive:
(if theme.css doesn't exist, see filament documentation
@source '../../../../vendor/mddev31/filament-dynamic-dashboard/resources/views/**/*';
Optionally publish the configuration file:
php artisan vendor:publish --tag=filament-dynamic-dashboard-config
Optionally publish translations:
php artisan vendor:publish --tag=filament-dynamic-dashboard-translations
Create a Filament page that extends DynamicDashboard. All standard Filament Page features (navigation icon, slug, group, etc.) remain available.
namespace App\Filament\Pages;
use MDDev\DynamicDashboard\Pages\DynamicDashboard;
class Dashboard extends DynamicDashboard
{
}
| Method | Signature | Purpose |
|---|---|---|
getDashboardFilters() |
static array |
Return Filament Field components shown in the filter bar |
getDefaultFilterSchema() |
static array |
Return custom fields for editing default filter values (keyed by filter name) |
resolveFilterDefaults() |
static array |
Transform stored defaults into actual filter values at apply time |
getColumns() |
int|array |
Grid columns for the widget layout (defaults to config) |
widgetsGrid() |
Component |
Override the grid layout used to render widgets |
canEdit() |
static bool |
Whether the current user can add/edit/delete widgets and manage dashboards |
canDisplay() |
static bool |
Whether the current user can view a specific dashboard |
showWidgetLoader() |
static bool |
Whether to show loading indicators on widgets (default: true) |
Any Filament Widget can become a dynamic widget by implementing the DynamicWidget interface. This requires three static methods:
| Method | Return Type | Purpose |
|---|---|---|
getWidgetLabel() |
string |
Display name shown in the widget type selector |
getSettingsFormSchema() |
array<Component> |
Filament form components for widget-specific settings |
getSettingsCasts() |
array<string, string> |
Cast definitions for settings values (primitives, BackedEnums, arrays) |
namespace App\Filament\Widgets;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\Concerns\InteractsWithPageFilters;
use MDDev\DynamicDashboard\Contracts\DynamicWidget;
class SimpleStatsWidget extends StatsOverviewWidget implements DynamicWidget
{
use InteractsWithPageFilters;
public static function getWidgetLabel(): string
{
return 'Simple Stats';
}
public static function getSettingsFormSchema(): array
{
return [];
}
public static function getSettingsCasts(): array
{
return [];
}
protected function getStats(): array
{
// Access page filters via $this->pageFilters['country'] etc.
return [/* ... */];
}
}
namespace App\Filament\Widgets;
use App\Enums\ResultTypeEnum;
use App\Enums\GroupingEnum;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Component;
use Filament\Widgets\Concerns\InteractsWithPageFilters;
use Leandrocfe\FilamentApexCharts\Widgets\ApexChartWidget;
use MDDev\DynamicDashboard\Contracts\DynamicWidget;
class SalesChartWidget extends ApexChartWidget implements DynamicWidget
{
use InteractsWithPageFilters;
public ResultTypeEnum $resultType = ResultTypeEnum::GrossRevenue;
public GroupingEnum $groupBy = GroupingEnum::Channel;
public ?int $limit = 5;
public static function getWidgetLabel(): string
{
return 'Sales Chart';
}
/**
* @return array<Component>
*/
public static function getSettingsFormSchema(): array
{
return [
Select::make('resultType')
->label('Result type')
->options(ResultTypeEnum::class)
->required()
->default(ResultTypeEnum::GrossRevenue->value),
Select::make('groupBy')
->label('Group by')
->options(GroupingEnum::class)
->required()
->default(GroupingEnum::Channel->value),
TextInput::make('limit')
->label('Limit')
->numeric()
->required()
->default(5),
];
}
/**
* @return array<string, string|array{0: string, 1: class-string}>
*/
public static function getSettingsCasts(): array
{
return [
'resultType' => ResultTypeEnum::class, // BackedEnum
'groupBy' => GroupingEnum::class, // BackedEnum
'limit' => 'int', // Primitive
];
}
protected function getOptions(): array
{
// $this->resultType, $this->groupBy, $this->limit are cast automatically
// $this->pageFilters contains dashboard filters
return [/* ... */];
}
}
The three pieces — public properties, form schema, and casts — are linked by a shared key name:
| Piece | Role | Example |
|---|---|---|
public ResultTypeEnum $resultType |
Livewire property that receives the value at render time | The widget reads $this->resultType |
Select::make('resultType') in getSettingsFormSchema() |
Form field the admin fills in (stored as JSON in the database) | Key resultType is saved in the settings JSON column |
'resultType' => ResultTypeEnum::class in getSettingsCasts() |
Type-cast rule applied when reading the JSON back | Raw string is converted to a BackedEnum |
The key name must be identical across all three. The form field name becomes the JSON key in the database, which is then cast and injected into the matching public property on the Livewire widget component.
Admin saves form
→ settings stored as JSON {"resultType": "gross_revenue", "limit": 5}
→ on render, AsWidgetSettings cast applies getSettingsCasts()
→ cast values spread into Widget::make(['resultType' => ResultTypeEnum::GrossRevenue, 'limit' => 5, ...])
→ Livewire hydrates public properties $this->resultType, $this->limit
Tip: Always give your public properties a default value. If a setting is not yet saved in the database, the default on the property is used.
The getSettingsCasts() method defines how stored JSON values are hydrated:
| Cast | Example | Description |
|---|---|---|
'int', 'integer' |
'limit' => 'int' |
Cast to integer |
'float', 'double' |
'ratio' => 'float' |
Cast to float |
'string' |
'label' => 'string' |
Cast to string |
'bool', 'boolean' |
'enabled' => 'bool' |
Cast to boolean |
MyEnum::class |
'type' => ResultTypeEnum::class |
Cast to a BackedEnum via tryFrom() |
['array', MyEnum::class] |
'types' => ['array', ResultTypeEnum::class] |
Cast each element of an array to a BackedEnum |
Implement the optional availableForDashboard() method to limit which dashboard pages can use the widget:
public static function availableForDashboard(): array
{
return [
\App\Filament\Pages\Dashboard::class,
// Widget will only appear on these dashboard pages
];
}
An empty array (or omitting the method entirely) means the widget is available on all dynamic dashboards.
Filament's canView() method is respected automatically. If canView() returns false, the widget is hidden from the type selector and not rendered on the dashboard.
Widgets can access their own ID and title by declaring public properties. The dashboard will automatically inject these values:
class MyWidget extends StatsOverviewWidget implements DynamicWidget
{
use InteractsWithPageFilters;
public int $dynamicDashboardWidgetId;
public string $dynamicDashboardWidgetTitle;
protected function getStats(): array
{
// Use $this->dynamicDashboardWidgetId or $this->dynamicDashboardWidgetTitle
return [/* ... */];
}
// ... other methods
}
Both properties are optional — declare only the ones you need.
Each widget displays a loading overlay while its Livewire component is updating. This behaviour is enabled by default and can be toggled globally or per-widget.
Override showWidgetLoader() in your dashboard subclass to disable the loader for all widgets:
class Dashboard extends DynamicDashboard
{
public static function showWidgetLoader(): bool
{
return false;
}
}
Add an optional static showLoader() method on any widget class. No interface change is required.
class HeavyChartWidget extends ApexChartWidget implements DynamicWidget
{
/**
* Force-enable or disable the loading indicator for this widget.
* Return null to use the dashboard default.
*/
public static function showLoader(): ?bool
{
return false; // disable loader for this widget
}
// ... other methods
}
showLoader() method and it returns a non-null boolean, that value is used.showWidgetLoader() value is used (default: true).This means a widget can force-enable the loader (return true) even when the dashboard default is false, or disable it (return false) when the dashboard default is true.
Templates define reusable layout structures for your dashboards. Each template contains positions where widgets can be placed.
The system simplifies the UI by hiding unnecessary selectors:
This means for simple setups with a single template and single position, users only need to select their widget type — no extra configuration required.
Override getDashboardFilters() to return an array of Filament Field components:
public static function getDashboardFilters(): array
{
return [
Select::make('country')
->label('Country')
->options(Country::pluck('name', 'id'))
->multiple()
->searchable(),
DatePicker::make('start_date')
->label('Start date'),
];
}
Each dashboard stores its filters independently in the session (keyed by page class and dashboard ID). Switching dashboards restores the last-used filters for that dashboard.
Admins can toggle which filters are visible for each dashboard from the Visible filters tab in the dashboard manager.
Default filter values are stored in the dashboard's filters JSON column. They are applied on first visit or when the user clicks the reset button.
Override getDefaultFilterSchema() to provide alternative field types for editing defaults. For example, a relative date selector instead of an absolute date picker:
public static function getDefaultFilterSchema(): array
{
return [
'period' => Select::make('period')
->label('Default period')
->options([
'this_month' => 'This month',
'last_month' => 'Last month',
'last_7_days' => 'Last 7 days',
'last_30_days' => 'Last 30 days',
]),
];
}
Filters not present in this array fall back to their original component from getDashboardFilters().
Override resolveFilterDefaults() to transform stored defaults into actual filter values:
public static function resolveFilterDefaults(array $defaults): array
{
if (!empty($defaults['period']) && is_string($defaults['period'])) {
$defaults['period'] = match ($defaults['period']) {
'this_month' => now()->startOfMonth()->format('Y-m-d').' - '.now()->format('Y-m-d'),
'last_30_days' => now()->subDays(29)->format('Y-m-d').' - '.now()->format('Y-m-d'),
default => $defaults['period'],
};
}
return $defaults;
}
Widgets access page filters through Filament's InteractsWithPageFilters trait:
use Filament\Widgets\Concerns\InteractsWithPageFilters;
class MyWidget extends StatsOverviewWidget implements DynamicWidget
{
use InteractsWithPageFilters;
protected function getStats(): array
{
$country = $this->pageFilters['country'] ?? null;
// ...
}
}
The filter bar includes a reset button. Clicking it calls resetFilters(), which re-applies the dashboard's stored defaults (or clears filters if none are configured).
A dropdown button in the page header lets users switch between dashboards. The current dashboard is highlighted with a check icon. An additional Manage dashboards entry (visible to editors) opens the management slideover.

The Add Widget button (visible to editors on unlocked dashboards) opens a modal with:
DynamicWidget implementationsgetSettingsFormSchema()
Each widget is wrapped with a hover overlay revealing edit and delete icon buttons. When Display title is enabled, a title badge is shown above the widget.
The dashboard manager slideover contains two tabs: Dashboards and Templates.
A reorderable table of all dashboards with:




Manage layout templates with:

Only one template can be marked as Default at a time.


Override canEdit() to restrict who can manage dashboards and widgets. When false, the add widget button, widget edit/delete overlays, and the manage dashboards entry are hidden.
public static function canEdit(): bool
{
return auth()->user()?->hasRole('admin') ?? false;
}
Override canDisplay() to control per-dashboard visibility. The default logic is:
canEdit() === true) always see all dashboardsuser->hasAnyRole(dashboard->roles)canAccess()public static function canDisplay(DynamicDashboardModel $dashboard): bool
{
// Custom logic example
if ($dashboard->getName() === 'Internal') {
return auth()->user()?->is_staff ?? false;
}
return parent::canDisplay($dashboard);
}
use_spatie_permissions to true in the configDashboardWithRoles model is automatically swapped in (adds the HasRoles trait)canDisplay() checks user->hasAnyRole(dashboard->roles) when roles are assignedPublish the config file:
php artisan vendor:publish --tag=filament-dynamic-dashboard-config
| Key | Type | Default | Description |
|---|---|---|---|
dashboard_columns |
array |
['sm' => 3, 'md' => 6, 'lg' => 12] |
Responsive grid breakpoints for the dashboard layout |
widget_columns |
int |
3 |
Default grid column span for new widgets |
use_spatie_permissions |
bool |
false |
Enable Spatie role integration |
Supported languages: English (en), French (fr), Spanish (es), Portuguese (pt), German (de), Russian (ru), Chinese (zh), Bulgarian (bg), Croatian (hr), Danish (da), Estonian (et), Finnish (fi), Greek (el), Hungarian (hu), Italian (it), Dutch (nl), Polish (pl), Romanian (ro), Swedish (sv), Czech (cs), Japanese (ja), Arabic (ar), Turkish (tr).
Publish translations to customize them:
php artisan vendor:publish --tag=filament-dynamic-dashboard-translations
All translation keys are namespaced under filament-dynamic-dashboard::dashboard.*.
See CHANGELOG.md for release notes.
Special thanks to :
The MIT License (MIT). See LICENSE.md for details.