Technical Documentation: filamentphp/widgets
โน๏ธ Provenance: Multi-Tier Ingestion ยท CodeWiki Reference
01 Overview (`docs/01-overview.md`)
title: Overview
Introduction
Filament allows you to build dynamic dashboards, comprised of "widgets". Each widget is an element on the dashboard that displays data in a specific way. For example, you can display stats, chart, or a table.
Creating a widget
To create a widget, you can use the make:filament-widget command:
php artisan make:filament-widget MyWidgetThis command will ask you which type of widget you want to create. You can choose from the following options:
- Custom: A custom widget that you can build from scratch.
- Chart: A widget that displays a chart.
- Stats overview: A widget that displays statistics.
- Table: A widget that displays a table.
Sorting widgets
Each widget class contains a $sort property that may be used to change its order on the page, relative to other widgets:
protected static ?int $sort = 2;Customizing the dashboard page
If you want to customize the dashboard class, for example, to change the number of widget columns, create a new file at app/Filament/Pages/Dashboard.php:
<?php
namespace App\Filament\Pages;
use Filament\Pages\Dashboard as BaseDashboard;
class Dashboard extends BaseDashboard
{
// ...
}Finally, remove the original Dashboard class from configuration file:
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->discoverPages(in: app_path('Filament/Pages'), for: 'App\\Filament\\Pages')
->pages([]);
}If you don't discover pages with discoverPages() in the directory you created the new dashboard class, you should manually register the class in the pages() method:
use App\Filament\Pages\Dashboard;
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->pages([
Dashboard::class,
]);
}Creating multiple dashboards
If you want to create multiple dashboards, you can do so by repeating the process described above. Creating new pages that extend the Dashboard class will allow you to create as many dashboards as you need.
You will also need to define the URL path to the extra dashboard, otherwise it will be at /:
protected static string $routePath = 'finance';You may also customize the title of the dashboard by overriding the $title property:
protected static ?string $title = 'Finance dashboard';The primary dashboard shown to a user is the first one they have access to (controlled by canAccess() method), according to the defined navigation sort order.
The default sort order for dashboards is -2. You can control the sort order of custom dashboards with $navigationSort:
protected static ?int $navigationSort = 15;Customizing the widgets' grid
You may change how many grid columns are used to display widgets.
Firstly, you must replace the original Dashboard page.
Now, in your new app/Filament/Pages/Dashboard.php file, you may override the getColumns() method to return a number of grid columns to use:
public function getColumns(): int | array
{
return 2;
}Responsive widgets grid
You may wish to change the number of widget grid columns based on the responsive breakpoint of the browser. You can do this using an array that contains the number of columns that should be used at each breakpoint:
public function getColumns(): int | array
{
return [
'md' => 4,
'xl' => 5,
];
}This pairs well with responsive widget widths.
Customizing widget width
You may customize the width of a widget using the $columnSpan property. You may use a number between 1 and 12 to indicate how many columns the widget should span, or full to make it occupy the full width of the page:
protected int | string | array $columnSpan = 'full';Responsive widget widths
You may wish to change the widget width based on the responsive breakpoint of the browser. You can do this using an array that contains the number of columns that the widget should occupy at each breakpoint:
protected int | string | array $columnSpan = [
'md' => 2,
'xl' => 3,
];This is especially useful when using a responsive widgets grid.
Conditionally hiding widgets
You may override the static canView() method on widgets to conditionally hide them:
public static function canView(): bool
{
return auth()->user()->isAdmin();
}Filament also checks canView() before a widget's mount() method runs, including when a lazy widget loads, and before subsequent Livewire updates. If access is denied, the request returns a 403 response. Dashboard and resource pages filter out denied widgets before mounting them.
Keep sensitive work out of the widget's boot() and lazy placeholder() methods, which can run before this authorization check. If you need sensitive work in these methods, authorize it separately.
Table widgets
You may easily add tables to your dashboard. Start by creating a widget with the command:
php artisan make:filament-widget LatestOrders --tableYou may now customize the table by editing the widget file.
Custom widgets
To get started building a BlogPostsOverview widget:
php artisan make:filament-widget BlogPostsOverviewThis command will create two files - a widget class in the /Widgets directory of the Filament directory, and a view in the /widgets directory of the Filament views directory.
The class is a Livewire component, so any Livewire features are available to you. The Blade view can contain any HTML you like, and you can access any public Livewire properties in the view. You can also access the Livewire component instance in the view using $this.
You can also render widgets with React, Vue, Svelte, or framework-free JavaScript. Add --react, --vue, --svelte, or --js to the command to generate a renderer instead of a Blade view. See custom widgets for the renderer lifecycle, PHP props, and dashboard filter synchronization.
Filtering widget data
You may add a form to the dashboard that allows the user to filter the data displayed across all widgets. When the filters are updated, the widgets will be reloaded with the new data.
Firstly, you must replace the original Dashboard page.
Now, in your new app/Filament/Pages/Dashboard.php file, you may add the HasFiltersForm trait, and add the filtersForm() method to return form components:
use Filament\Forms\Components\DatePicker;
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Concerns\HasFiltersForm;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class Dashboard extends BaseDashboard
{
use HasFiltersForm;
public function filtersForm(Schema $schema): Schema
{
return $schema
->components([
Section::make()
->schema([
DatePicker::make('startDate'),
DatePicker::make('endDate'),
// ...
])
->columns(3),
]);
}
}In widget classes that require data from the filters, you need to add the InteractsWithPageFilters trait, which will allow you to use the $this->pageFilters property to access the raw data from the filters form:
use App\Models\BlogPost;
use Carbon\CarbonImmutable;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\Concerns\InteractsWithPageFilters;
use Illuminate\Database\Eloquent\Builder;
class BlogPostsOverview extends StatsOverviewWidget
{
use InteractsWithPageFilters;
public function getStats(): array
{
$startDate = $this->pageFilters['startDate'] ?? null;
$endDate = $this->pageFilters['endDate'] ?? null;
return [
StatsOverviewWidget\Stat::make(
label: 'Total posts',
value: BlogPost::query()
->when($startDate, fn (Builder $query) => $query->whereDate('created_at', '>=', $startDate))
->when($endDate, fn (Builder $query) => $query->whereDate('created_at', '<=', $endDate))
->count(),
),
// ...
];
}
}The $this->pageFilters array will always reflect the current form data. Please note that this data is not validated, as it is available live and not intended to be used for anything other than querying the database. You must ensure that the data is valid before using it. In this example, we check if the start date is set before using it in the query.
Filtering widget data using an action modal
Alternatively, you can swap out the filters form for an action modal, that can be opened by clicking a button in the header of the page. There are many benefits to using this approach:
- The filters form is not always visible, which allows you to use the full height of the page for widgets.
- The filters do not update the widgets until the user clicks the "Apply" button, which means that the widgets are not reloaded until the user is ready. This can improve performance if the widgets are expensive to load.
- Validation can be performed on the filters form, which means that the widgets can rely on the fact that the data is valid - the user cannot submit the form until it is. Canceling the modal will discard the user's changes.
To use an action modal instead of a filters form, you can use the HasFiltersAction trait instead of HasFiltersForm. Then, register the FilterAction class as an action in getHeaderActions():
use Filament\Forms\Components\DatePicker;
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Actions\FilterAction;
use Filament\Pages\Dashboard\Concerns\HasFiltersAction;
class Dashboard extends BaseDashboard
{
use HasFiltersAction;
protected function getHeaderActions(): array
{
return [
FilterAction::make()
->schema([
DatePicker::make('startDate'),
DatePicker::make('endDate'),
// ...
]),
];
}
}Handling data from the filter action is the same as handling data from the filters header form, except that the data is validated before being passed to the widget. The InteractsWithPageFilters trait still applies.
Persisting widget filters in the user's session
By default, the dashboard filters applied will persist in the user's session between page loads. If you use tenancy, filters are persisted separately for each tenant. To disable persistence, override the $persistsFiltersInSession property in the dashboard page class:
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Concerns\HasFiltersForm;
class Dashboard extends BaseDashboard
{
use HasFiltersForm;
protected bool $persistsFiltersInSession = false;
}Alternatively, override the persistsFiltersInSession() method in the dashboard page class:
use Filament\Pages\Dashboard as BaseDashboard;
use Filament\Pages\Dashboard\Concerns\HasFiltersForm;
class Dashboard extends BaseDashboard
{
use HasFiltersForm;
public function persistsFiltersInSession(): bool
{
return false;
}
}Disabling the default widgets
By default, two widgets are displayed on the dashboard. These widgets can be disabled by updating the widgets() array of the configuration:
use Filament\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->widgets([]);
}02 Stats Overview (`docs/02-stats-overview.md`)
title: Stats overview widgets
Introduction
Filament comes with a "stats overview" widget template, which you can use to display a number of different stats in a single widget, without needing to write a custom view.
Start by creating a widget with the command:
php artisan make:filament-widget StatsOverview --stats-overviewThis command will create a new StatsOverview.php file. Open it, and return Stat instances from the getStats() method:
<?php
namespace App\Filament\Widgets;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
class StatsOverview extends BaseWidget
{
protected function getStats(): array
{
return [
Stat::make('Unique views', '192.1k'),
Stat::make('Bounce rate', '21%'),
Stat::make('Average time on page', '3:12'),
];
}
}Now, check out your widget in the dashboard.
Adding a description and icon to a stat
You may add a description() to provide additional information, along with a descriptionIcon():
use Filament\Widgets\StatsOverviewWidget\Stat;
protected function getStats(): array
{
return [
Stat::make('Unique views', '192.1k')
->description('32k increase')
->descriptionIcon('heroicon-m-arrow-trending-up'),
Stat::make('Bounce rate', '21%')
->description('7% decrease')
->descriptionIcon('heroicon-m-arrow-trending-down'),
Stat::make('Average time on page', '3:12')
->description('3% increase')
->descriptionIcon('heroicon-m-arrow-trending-up'),
];
}The descriptionIcon() method also accepts a second parameter to put the icon before the description instead of after it:
use Filament\Support\Enums\IconPosition;
use Filament\Widgets\StatsOverviewWidget\Stat;
Stat::make('Unique views', '192.1k')
->description('32k increase')
->descriptionIcon('heroicon-m-arrow-trending-up', IconPosition::Before)Changing the color of the stat
You may also give stats a color:
use Filament\Widgets\StatsOverviewWidget\Stat;
protected function getStats(): array
{
return [
Stat::make('Unique views', '192.1k')
->description('32k increase')
->descriptionIcon('heroicon-m-arrow-trending-up')
->color('success'),
Stat::make('Bounce rate', '21%')
->description('7% increase')
->descriptionIcon('heroicon-m-arrow-trending-down')
->color('danger'),
Stat::make('Average time on page', '3:12')
->description('3% increase')
->descriptionIcon('heroicon-m-arrow-trending-up')
->color('success'),
];
}Adding extra HTML attributes to a stat
You may also pass extra HTML attributes to stats using extraAttributes():
use Filament\Widgets\StatsOverviewWidget\Stat;
protected function getStats(): array
{
return [
Stat::make('Processed', '192.1k')
->color('success')
->extraAttributes([
'class' => 'cursor-pointer',
'wire:click' => "\$dispatch('setStatusFilter', { filter: 'processed' })",
]),
// ...
];
}In this example, we are deliberately escaping the $ in $dispatch() since this needs to be passed directly to the HTML, it is not a PHP variable.
Setting a placeholder for a stat
Sometimes a stat's value may not be available โ for example, when a dashboard has nothing to show for a specific period. You can use placeholder() to define what should be displayed instead when the value is blank:
use Filament\Widgets\StatsOverviewWidget\Stat;
Stat::make('Unique views', $uniqueViews)
->placeholder('-')A value is considered blank according to Laravel's blank() helper. For example, null and empty strings are blank, while 0 and '0' are not.
Setting a default placeholder globally
If you'd like every stat in your app to fall back to the same placeholder by default, you can use the static configureUsing() method in a service provider's boot() method:
use Filament\Widgets\StatsOverviewWidget\Stat;
Stat::configureUsing(function (Stat $component): void {
$component->placeholder('-');
});Individual stats can still override this by calling placeholder() themselves.
Adding a chart to a stat
You may also add or chain a chart() to each stat to provide historical data. The chart() method accepts an array of data points to plot:
use Filament\Widgets\StatsOverviewWidget\Stat;
protected function getStats(): array
{
return [
Stat::make('Unique views', '192.1k')
->description('32k increase')
->descriptionIcon('heroicon-m-arrow-trending-up')
->chart([7, 2, 10, 3, 15, 4, 17])
->color('success'),
// ...
];
}Styling stat charts in a theme
To style stat charts from a custom theme, set these CSS custom properties on .fi-wi-stats-overview-stat or a parent element:
.fi-wi-stats-overview-stat {
--stat-chart-border-width: 1;
--stat-chart-line-tension: 0;
--stat-chart-fill: none;
}--stat-chart-border-width: The line thickness.--stat-chart-line-tension: The curvature, from0for straight segments to1.--stat-chart-fill: The fill target:start,end,origin, orstack. Usenoneto disable the fill.
Use unitless numbers and the supported keywords, not CSS lengths. You may set different values for light and dark mode.
The chart takes its colors from the color of the stat. To change them in a theme, style the .fi-wi-stats-overview-stat-chart-bg-color and .fi-wi-stats-overview-stat-chart-border-color elements with an ordinary color declaration:
.fi-wi-stats-overview-stat {
& .fi-wi-stats-overview-stat-chart-border-color {
@apply text-gray-400 dark:text-gray-500;
}
}Live updating stats (polling)
By default, stats overview widgets refresh their data every 5 seconds.
To customize this, you may override the $pollingInterval property on the class to a new interval:
protected ?string $pollingInterval = '10s';Alternatively, you may disable polling altogether:
protected ?string $pollingInterval = null;Disabling lazy loading
By default, widgets are lazy-loaded. This means that they will only be loaded when they are visible on the page.
To disable this behavior, you may override the $isLazyLoaded property on the widget class:
protected static ?bool $isLazyLoaded = false;Adding a heading and description
You may also add heading and description text above the widget by overriding the $heading and $description properties:
protected ?string $heading = 'Analytics';
protected ?string $description = 'An overview of some analytics.';If you need to dynamically generate the heading or description text, you can instead override the getHeading() and getDescription() methods:
protected function getHeading(): ?string
{
return 'Analytics';
}
protected function getDescription(): ?string
{
return 'An overview of some analytics.';
}03 Charts (`docs/03-charts.md`)
title: Chart widgets
Introduction
Filament comes with many "chart" widget templates, which you can use to display real-time, interactive charts.
Start by creating a widget with the command:
php artisan make:filament-widget BlogPostsChart --chartThere is a single ChartWidget class that is used for all charts. The type of chart is set by the getType() method. In this example, that method returns the string 'line'.
The protected ?string $heading variable is used to set the heading that describes the chart. If you need to set the heading dynamically, you can override the getHeading() method.
The getData() method is used to return an array of datasets and labels. Each dataset is a labeled array of points to plot on the chart, and each label is a string. This structure is identical to the Chart.js library, which Filament uses to render charts. You may use the Chart.js documentation to fully understand the possibilities to return from getData(), based on the chart type.
<?php
namespace App\Filament\Widgets;
use Filament\Widgets\ChartWidget;
class BlogPostsChart extends ChartWidget
{
protected ?string $heading = 'Blog Posts';
protected function getData(): array
{
return [
'datasets' => [
[
'label' => 'Blog posts created',
'data' => [0, 10, 5, 2, 21, 32, 45, 74, 65, 45, 77, 89],
],
],
'labels' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
];
}
protected function getType(): string
{
return 'line';
}
}Now, check out your widget in the dashboard.
Available chart types
Below is a list of available chart widget classes which you may extend, and their corresponding Chart.js documentation page, for inspiration on what to return from getData():
- Bar chart - Chart.js documentation
- Bubble chart - Chart.js documentation
- Doughnut chart - Chart.js documentation
- Line chart - Chart.js documentation
- Pie chart - Chart.js documentation
- Polar area chart - Chart.js documentation
- Radar chart - Chart.js documentation
- Scatter chart - Chart.js documentation
For example, you could use a bar chart by returning 'bar' from the getType() method:
Here are examples of the other available chart types:
Customizing the chart color
You can customize the color of the chart data by setting the $color property:
protected string $color = 'info';If you're looking to customize the color further, or use multiple colors across multiple datasets, you can still make use of Chart.js's color options in the data:
protected function getData(): array
{
return [
'datasets' => [
[
'label' => 'Blog posts created',
'data' => [0, 10, 5, 2, 21, 32, 45, 74, 65, 45, 77, 89],
'backgroundColor' => '#36A2EB',
'borderColor' => '#9BD0F5',
],
],
'labels' => ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
];
}Generating chart data from an Eloquent model
To generate chart data from an Eloquent model, Filament recommends that you install the flowframe/laravel-trend package. You can view the documentation.
Here is an example of generating chart data from a model using the laravel-trend package:
use Flowframe\Trend\Trend;
use Flowframe\Trend\TrendValue;
protected function getData(): array
{
$data = Trend::model(BlogPost::class)
->between(
start: now()->startOfYear(),
end: now()->endOfYear(),
)
->perMonth()
->count();
return [
'datasets' => [
[
'label' => 'Blog posts',
'data' => $data->map(fn (TrendValue $value) => $value->aggregate),
],
],
'labels' => $data->map(fn (TrendValue $value) => $value->date),
];
}Filtering chart data
Basic Select filter
You can set up chart filters to change the data that is presented. Commonly, this is used to change the time period that chart data is rendered for.
To set a default filter value, set the $filter property:
public ?string $filter = 'today';Then, define the getFilters() method to return an array of values and labels for your filter:
protected function getFilters(): ?array
{
return [
'today' => 'Today',
'week' => 'Last week',
'month' => 'Last month',
'year' => 'This year',
];
}You can use the active filter value within your getData() method:
protected function getData(): array
{
$activeFilter = $this->filter;
// ...
}Custom filters
You can use schema components to build custom filters for your chart widget. This approach offers a more flexible way to define filters.
To get started, use the HasFiltersSchema trait and implement the filtersSchema() method:
use Filament\Forms\Components\DatePicker;
use Filament\Schemas\Schema;
use Filament\Widgets\ChartWidget\Concerns\HasFiltersSchema;
class BlogPostsChart extends ChartWidget
{
use HasFiltersSchema;
// ...
public function filtersSchema(Schema $schema): Schema
{
return $schema->components([
DatePicker::make('startDate')
->default(now()->subDays(30)),
DatePicker::make('endDate')
->default(now()),
]);
}
}The filter values are accessible via the $this->filters array. You can use these values inside your getData() method:
protected function getData(): array
{
$startDate = $this->filters['startDate'] ?? null;
$endDate = $this->filters['endDate'] ?? null;
return [
// ...
];
}The $this->filters array will always reflect the current form data. Please note that this data is not validated, as it is available live and not intended to be used for anything other than querying the database. You must ensure that the data is valid before using it.
Deferring filter updates
By default, filters using the filtersSchema() method update the chart data immediately as they are changed. However, for complex queries or better user experience, you may want to defer filter updates until the user clicks an "Apply" button.
When deferred, filter changes are only applied when the user clicks the "Apply" button. This ensures that the chart only re-renders when the user has finished adjusting all of their filters.
The chart will display data using the default filter values when the page first loads, ensuring users see meaningful data immediately without needing to take action.
To enable deferred filters, set the $hasDeferredFilters property to true:
use Filament\Widgets\ChartWidget\Concerns\HasFiltersSchema;
class BlogPostsChart extends ChartWidget
{
use HasFiltersSchema;
protected bool $hasDeferredFilters = true;
// ...
}If you need dynamic control over whether filters are deferred, you may override the hasDeferredFilters() method:
public function hasDeferredFilters(): bool
{
return auth()->user()->prefersDeferredFilters();
}Resetting filters to defaults
When using deferred filters, a "Reset" link appears in the filter dropdown footer alongside the "Apply" button. Clicking this link restores all filters to their default values as defined in the filtersSchema() method. For example, if you set ->default(now()->subDays(30)) on a DatePicker, the reset action will restore that default date, not an empty value.
Customizing filter actions
You may customize the apply and reset actions that appear when using deferred filters. All methods that are available to customize action trigger buttons can be used:
use Filament\Actions\Action;
public function filtersApplyAction(Action $action): Action
{
return $action
->label('Update Chart')
->color('success');
}
public function filtersResetAction(Action $action): Action
{
return $action
->label('Clear Filters')
->color('danger');
}Empty state
When the getData() method returns an empty array, the chart widget renders an "empty state" instead of the chart.
To customize when the empty state is rendered, override the isEmpty() method:
public function isEmpty(): bool
{
$data = $this->getCachedData();
return empty($data['datasets'][0]['data'] ?? []);
}Setting the empty state heading
To customize the heading of the empty state, set the $emptyStateHeading property:
protected ?string $emptyStateHeading = 'No data available';Alternatively, you can override the getEmptyStateHeading() method to return a dynamic heading:
use Illuminate\Contracts\Support\Htmlable;
public function getEmptyStateHeading(): string | Htmlable
{
return "No sales yet for {$this->filter}";
}Setting the empty state description
To customize the description of the empty state, set the $emptyStateDescription property:
protected ?string $emptyStateDescription = 'Check back later once data has been collected.';Alternatively, you can override the getEmptyStateDescription() method to return a dynamic description:
use Illuminate\Contracts\Support\Htmlable;
public function getEmptyStateDescription(): string | Htmlable | null
{
return 'Sales data will appear here once orders are placed.';
}Setting the empty state icon
To customize the icon of the empty state, set the $emptyStateIcon property:
use Filament\Support\Icons\Heroicon;
protected string | BackedEnum | null $emptyStateIcon = Heroicon::OutlinedChartBar;Alternatively, you can override the getEmptyStateIcon() method to return a dynamic icon:
use BackedEnum;
use Filament\Support\Icons\Heroicon;
use Illuminate\Contracts\Support\Htmlable;
public function getEmptyStateIcon(): string | BackedEnum | Htmlable
{
return Heroicon::OutlinedShoppingCart;
}Adding empty state actions
You can add actions to the empty state to prompt users to take action by overriding the getEmptyStateActions() method:
use Filament\Actions\Action;
public function getEmptyStateActions(): array
{
return [
Action::make('refresh')
->label('Refresh')
->action('refresh'),
];
}Using a custom empty state view
You may use a completely custom empty state view by overriding the getEmptyState() method:
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Contracts\View\View;
public function getEmptyState(): View | Htmlable | null
{
return view('widgets.charts.custom-empty-state');
}Live updating chart data (polling)
By default, chart widgets refresh their data every 5 seconds.
To customize this, you may override the $pollingInterval property on the class to a new interval:
protected ?string $pollingInterval = '10s';Alternatively, you may disable polling altogether:
protected ?string $pollingInterval = null;Setting a maximum chart height
You may place a maximum height on the chart to ensure that it doesn't get too big, using the $maxHeight property:
protected ?string $maxHeight = '300px';Setting chart configuration options
You may specify an $options variable on the chart class to control the many configuration options that the Chart.js library provides. For instance, you could turn off the legend for a line chart:
protected ?array $options = [
'plugins' => [
'legend' => [
'display' => false,
],
],
];Alternatively, you can override the getOptions() method to return a dynamic array of options:
protected function getOptions(): array
{
return [
'plugins' => [
'legend' => [
'display' => false,
],
],
];
}These PHP arrays will get transformed into JSON objects when the chart is rendered. If you want to return raw JavaScript from this method instead, you can return a RawJs object. This is useful if you want to use a JavaScript callback function, for example:
use Filament\Support\RawJs;
protected function getOptions(): RawJs
{
return RawJs::make(<<<JS
{
scales: {
y: {
ticks: {
callback: (value) => 'โฌ' + value,
},
},
},
}
JS);
}Styling charts in a theme
To style the canvas-rendered chart from a custom theme, set these CSS custom properties on .fi-wi-chart or a parent element:
.fi-wi-chart {
--chart-border-width: 1;
--chart-line-tension: 0.4;
--chart-point-radius: 3;
--chart-point-style: rect;
--chart-bar-border-radius: 4;
}--chart-border-width: The thickness of the data outlines.--chart-line-tension: The curvature of a line chart, from0for straight segments to1.--chart-point-radius: The marker size for line, radar, and scatter charts.--chart-point-style: The marker shape:circle,cross,crossRot,dash,line,rect,rectRounded,rectRot,star, ortriangle. Usenoneto hide markers.--chart-bar-border-radius: The corner radius of bars. Use0for square corners instead of the default rounded corners.
Use unitless numbers and the supported keywords, not CSS lengths such as 3px. You may set different values for light and dark mode.
Styling the chart legend
The legend beneath a chart is drawn onto the canvas as well. Two properties control the color swatch next to each label:
.fi-wi-chart {
--chart-legend-box-width: 16;
--chart-legend-border-radius: 0;
}--chart-legend-box-width sets how wide each swatch is, and --chart-legend-border-radius rounds its corners, which are slightly rounded by default to match the bars of a bar chart. Set it to 0 for square swatches.
Styling chart tooltips
The tooltip that appears when hovering over a chart is drawn onto the canvas as well. Its shape comes from two more properties:
.fi-wi-chart {
--chart-tooltip-corner-radius: 0;
--chart-tooltip-border-width: 1;
}Its colors are set differently, so that you can use the same palette and dark mode variants as the rest of your theme. Filament reads them from elements that you style with an ordinary color declaration:
.fi-wi-chart {
& .fi-wi-chart-tooltip-bg-color {
@apply text-gray-900 dark:text-white;
}
& .fi-wi-chart-tooltip-text-color {
@apply text-white dark:text-gray-900;
}
& .fi-wi-chart-tooltip-border-color {
@apply text-gray-700 dark:text-gray-200;
}
}A tooltip has no border until you give it a width, so --chart-tooltip-border-width and .fi-wi-chart-tooltip-border-color usually change together.
The colors of the chart itself work in the same way: .fi-wi-chart-bg-color and .fi-wi-chart-border-color fill and outline the data, .fi-wi-chart-grid-color draws the grid lines, and .fi-wi-chart-text-color labels the axes.
The small charts inside a stats overview widget are styled separately, with their own set of properties.
Adding a description
You may add a description, below the heading of the chart, using the getDescription() method:
public function getDescription(): ?string
{
return 'The number of blog posts published per month.';
}Disabling lazy loading
By default, widgets are lazy-loaded. This means that they will only be loaded when they are visible on the page.
To disable this behavior, you may override the $isLazyLoaded property on the widget class:
protected static ?bool $isLazyLoaded = false;Making the chart collapsible
You may allow the chart to be collapsible by setting the $isCollapsible property on the widget class to be true:
protected bool $isCollapsible = true;Providing an accessible alternative
Chart.js renders the chart onto a <canvas>, so assistive technologies cannot read the plotted values or relationships between them. The widget's heading and description identify and summarize the chart. You should ensure that the description communicates the chart's purpose and important trends or relationships:
protected ?string $heading = 'Blog posts created';
protected ?string $description = 'Blog post creation peaked in August, before declining through October.';For charts where the summary does not provide an equivalent alternative, you can return additional content from the getChartAssistiveContent() method. This content is visually hidden but available to screen readers. You can return a string for a textual alternative:
public function getChartAssistiveContent(): string
{
return 'The number of blog posts created increased from 0 in January to 89 in December.';
}You may return a View or Htmlable instance to provide structured content, such as a data table. Avoid including links, buttons, or other interactive elements, since visually hidden controls would not be apparent to sighted keyboard users. The alternative is rendered from the same Livewire request as the chart, so you can use getCachedData() to keep it synchronized with the chart's data and applied filters:
use Illuminate\Contracts\View\View;
public function getChartAssistiveContent(): View
{
return view('filament.widgets.blog-posts-chart-assistive-content', [
'data' => $this->getCachedData(),
]);
}For the standard Chart.js structure of shared labels and multiple datasets, the view could render each dataset as a column:
<table>
<caption>Blog posts created each month</caption>
<thead>
<tr>
<th scope="col">Month</th>
@foreach ($data['datasets'] as $dataset)
<th scope="col">{{ $dataset['label'] }}</th>
@endforeach
</tr>
</thead>
<tbody>
@foreach ($data['labels'] as $index => $label)
<tr>
<th scope="row">{{ $label }}</th>
@foreach ($data['datasets'] as $dataset)
<td>{{ $dataset['data'][$index] }}</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>Chart.js also supports data points represented by tuples or objects, custom parsing keys, mixed dataset types, and plugins that transform data in the browser. For these structures, map the meaningful labels, values, units, and annotations explicitly instead of assuming that the example above applies. For a large dataset, prefer a concise summary or a meaningful subset of the data rather than rendering every point in a long, visually hidden table. If users need to explore or download the complete data, provide that as a separate visible interface.
When polling or filtering updates the chart, Filament updates the alternative without automatically announcing the entire replacement. If an update is important enough to announce immediately, provide a concise, appropriately throttled live region in your alternative instead of making the complete table a live region.
Using custom Chart.js plugins
Chart.js offers a powerful plugin system that allows you to extend its functionality and create custom chart behaviors. This guide details how to use them in a chart widget.
Step 1: Install the plugin with NPM
To start with, install the plugin using NPM into your project. In this guide, we will install chartjs-plugin-datalabels:
npm install chartjs-plugin-datalabels --save-devStep 2: Create a JavaScript file importing the plugin
Create a new JavaScript file where you will define your custom plugin. In this guide, we'll call it filament-chart-js-plugins.js. Import the plugin, and add it to the window.filamentChartJsPlugins array:
import ChartDataLabels from 'chartjs-plugin-datalabels'
window.filamentChartJsPlugins ??= []
window.filamentChartJsPlugins.push(ChartDataLabels)This is equivalent to including the plugins "inline" via new Chart(..., { plugins: [...] }) when instantiating a Chart.js chart.
It's important to initialise the array if it has not been already, before pushing onto it. This ensures that multiple JavaScript files (especially those from Filament plugins) that register Chart.js plugins do not overwrite each other, regardless of the order they are booted in.
You can push as many plugins to the array as you would like to install, you do not need a separate file to import each plugin.
Additionally, you can also register any "global plugins" which will use Chart.register([...]) in the window.filamentChartJsGlobalPlugins array:
import ChartDataLabels from 'chartjs-plugin-datalabels'
window.filamentChartJsGlobalPlugins ??= []
window.filamentChartJsGlobalPlugins.push(ChartDataLabels)Step 3: Compile the JavaScript file with Vite
Now, you need to build the JavaScript file with Vite, or your bundler of choice. Include the file in your Vite configuration (usually vite.config.js). For example:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/css/app.css',
'resources/js/app.js',
'resources/css/filament/admin/theme.css',
'resources/js/filament-chart-js-plugins.js', // Include the new file in the `input` array so it is built
],
}),
],
});Build the file with npm run build.
Step 4: Register the JavaScript file in Filament
Filament needs to know to include this JavaScript file when rendering chart widgets. You can do this in the boot() method of a service provider like AppServiceProvider:
use Filament\Support\Assets\Js;
use Filament\Support\Facades\FilamentAsset;
use Illuminate\Support\Facades\Vite;
FilamentAsset::register([
Js::make('chart-js-plugins', Vite::asset('resources/js/filament-chart-js-plugins.js'))->module(),
]);You can find out more about asset registration, and even register assets for a specific panel.
04 Custom Widgets (`docs/04-custom-widgets.md`)
title: Custom widgets
Introduction
You can create a reusable widget with a Blade view, React, Vue, Svelte, or framework-free JavaScript. Start by generating a widget, then customize its view or renderer. Filament handles the widget's grid placement and lazy loading.
To generate a Blade-based widget:
php artisan make:filament-widget RevenueOverviewChoose Custom, then edit the generated view. Widgets are Livewire components, so you can use public properties and methods in the view through $this and $wire.
Generating a JavaScript widget
Install your application's JavaScript dependencies, including Vite, before running the generator, for example with npm install.
Add one renderer flag to the command:
php artisan make:filament-widget RevenueOverview --react
php artisan make:filament-widget RevenueOverview --vue
php artisan make:filament-widget RevenueOverview --svelte
php artisan make:filament-widget RevenueOverview --jsThe command creates a PHP widget class and a renderer, normally in resources/js/filament/widgets/, instead of a Blade view. Replace the starter content with your own UI, which appears inside a standard Filament widget section.
The command installs missing dependencies, configures Vite where possible, and offers to compile your assets. Existing dependencies are preserved; incompatible versions require a manual upgrade. If your Vite configuration needs manual changes, follow the command's instructions and the Vite module setup.
For TypeScript, add --typescript or its alias --ts:
php artisan make:filament-widget RevenueOverview --react --tsRegister or discover the generated class in your panel like any other widget. You can also use it as a page widget or a resource widget.
Customizing generation
You can customize the command with these options:
--pm=yarnuses Yarn instead of npm.--skip-installskips dependency installation. Vite must already be installed.--skip-buildskips the compilation prompt.
The panel, resource, cluster, and --force options still apply. Renderer flags cannot be combined with --chart, --stats-overview, or --table.
The generated files depend on your chosen renderer:
- React:
revenue-overview.jsx, orrevenue-overview.tsxwith--typescript. - Vue:
revenue-overview.jsandRevenueOverview.vue. With--typescript, the entry uses.tsand the component uses<script setup lang="ts">. - Svelte 5:
revenue-overview.svelte.jsandRevenueOverview.svelte. With--typescript, the entry uses.svelte.tsand the component uses<script lang="ts">. - Framework-free JavaScript:
revenue-overview.js, orrevenue-overview.tswith--typescript.
The React component and Vue or Svelte import are named RevenueOverview, matching the PHP class. Names that start with a lowercase letter or conflict with a starter's imports use the local name Component instead. Filenames and the default-exported mountRevenueOverview() function still use the requested name.
Renderer directories follow the PHP namespace in kebab case, excluding your application's root namespace. For example, App\Filament\Resources\Orders\Widgets\RevenueOverview uses resources/js/filament/resources/orders/widgets/.
Passing configuration from PHP
Use getRendererConfiguration() to return JSON-serializable data. The renderer receives this data as props.configuration:
namespace App\Filament\Widgets;
use App\Models\Order;
use Filament\Widgets\Concerns\HasJsRenderer;
use Filament\Widgets\Widget;
use Illuminate\Support\Facades\Vite;
class RevenueOverview extends Widget
{
use HasJsRenderer;
public function getRenderer(): string
{
return Vite::asset('resources/js/filament/widgets/revenue-overview.js');
}
public function getRendererConfiguration(): array
{
return [
'heading' => 'Revenue overview',
'description' => 'Total revenue: ' . Order::query()->sum('total'),
];
}
}Use the entry extension generated for your framework. When the widget re-renders, Filament passes changed configuration to the renderer's update() method without remounting it. Changing props.configuration in JavaScript does not update PHP.
Receiving dashboard filter values
Add InteractsWithPageFilters to read the dashboard's reactive $pageFilters, just as you would in any other widget. Include the filter values, or data calculated from them, in getRendererConfiguration():
use Filament\Widgets\Concerns\InteractsWithPageFilters;
// Inside your widget class:
use InteractsWithPageFilters;
public function getRendererConfiguration(): array
{
return [
'heading' => 'Revenue overview',
'description' => 'Period starts: ' . ($this->pageFilters['startDate'] ?? 'All time'),
'filters' => $this->pageFilters,
];
}Set up the dashboard's filters form or filter action as usual. Only pass the filter values the renderer needs, and validate live form filters before using them in a query.
For other parent-provided properties, use Livewire's #[Reactive] attribute to trigger widget updates when they change.
Writing a renderer
A renderer module default-exports a function receiving { host, props, utilities }. Mount your UI inside host and return an object with update(props) and destroy() methods. The generated starters demonstrate each framework's lifecycle.
Creating a framework-free JavaScript widget
export default function mountRevenueOverview({ host, props: initialProps }) {
const heading = document.createElement('h2')
const description = document.createElement('p')
const update = ({ configuration }) => {
heading.textContent = configuration.heading
description.textContent = configuration.description
}
update(initialProps)
host.append(heading, description)
return { update, destroy: () => host.replaceChildren() }
}Creating a React widget
import React from 'react'
import { createRoot } from 'react-dom/client'
function RevenueOverview({ configuration }) {
return <><h2>{configuration.heading}</h2><p>{configuration.description}</p></>
}
export default function mountRevenueOverview({ host, props: initialProps }) {
const root = createRoot(host)
const update = (props) => root.render(<RevenueOverview {...props} />)
update(initialProps)
return { update, destroy: () => root.unmount() }
}Creating a Vue widget
Use a shallow ref to replace the PHP props without deeply proxying them:
import { createApp, h, shallowRef } from 'vue'
import RevenueOverview from './RevenueOverview.vue'
export default function mountRevenueOverview({ host, props: initialProps }) {
const props = shallowRef(initialProps)
const application = createApp({ setup: () => () => h(RevenueOverview, props.value) })
application.mount(host)
return {
update: (nextProps) => { props.value = nextProps },
destroy: () => application.unmount(),
}
}In RevenueOverview.vue:
<script setup>
defineProps(['configuration'])
</script>
<template>
<h2>{{ configuration.heading }}</h2>
<p>{{ configuration.description }}</p>
</template>Creating a Svelte widget
Use Svelte 5's reactive state in the .svelte.js entry:
import { mount, unmount } from 'svelte'
import RevenueOverview from './RevenueOverview.svelte'
export default function mountRevenueOverview({ host, props: initialProps }) {
const props = $state({ ...initialProps })
const component = mount(RevenueOverview, { target: host, props })
return {
update: (nextProps) => { Object.assign(props, nextProps) },
destroy: () => unmount(component),
}
}In RevenueOverview.svelte:
<script>
let { configuration } = $props()
</script>
<h2>{configuration.heading}</h2>
<p>{configuration.description}</p>Calling PHP methods
utilities.$wire is the widget's own Livewire proxy. Use it to call a public method, read a property, or update a property:
export default function mountRevenueOverview({ host, props, utilities }) {
const button = document.createElement('button')
button.type = 'button'
button.textContent = 'Refresh revenue'
button.onclick = () => utilities.$wire.$call('refreshRevenue')
host.append(button)
return { update() {}, destroy: () => host.replaceChildren() }
}Define refreshRevenue() on your widget class. Validate browser-provided arguments and authorize operations inside PHP methods. To call PHP from a React, Vue, or Svelte component, pass utilities to it as a prop.
Typing renderers
The generator adds the @filament/widgets/js-widget type alias to standard JSON tsconfig.json files. To configure it manually, add this mapping, adjusting the path relative to baseUrl if set:
{
"compilerOptions": {
"paths": {
"@filament/widgets/js-widget": [
"./vendor/filament/widgets/resources/js/types/js-widget.d.ts"
]
}
}
}JsWidgetRenderer<Configuration, Methods> types the PHP configuration and optional public widget methods. JsWidgetProps<Configuration>, JsWidgetRendererContext<Configuration, Methods>, and JsWidgetRendererInstance<Configuration> are also available:
import type { JsWidgetRenderer } from '@filament/widgets/js-widget'
type Configuration = { heading: string; total: number }
type Methods = { refreshRevenue(): Promise<void> }
const mountRevenueOverview: JsWidgetRenderer<Configuration, Methods> = ({ host, props, utilities }) => {
const update = ({ configuration }: typeof props) => {
host.textContent = `${configuration.heading}: ${configuration.total}`
}
update(props)
return { update, destroy: () => host.replaceChildren() }
}
export default mountRevenueOverviewVite builds do not check types. See type-checking ES modules for compiler setup and framework-specific commands.
Synchronizing and disposing renderers
Livewire does not manage the contents of host. Use update() to apply PHP configuration while preserving local UI state. Do not render Blade, Livewire, or Alpine components inside this subtree.
Mounting may be asynchronous: Filament delivers the latest props when it completes, or disposes the instance if the widget was removed. Changing getRenderer() replaces the renderer; changing getRendererConfiguration() updates it in place.
Use destroy() to unmount your framework and release listeners, observers, timers, and subscriptions. Stop DOM work immediately: Filament does not wait for asynchronous cleanup before remounting. Cancel or order overlapping asynchronous updates in your renderer.
Handling renderer errors
If importing, mounting, or updating fails, Filament logs the error, disposes the renderer, and replaces its contents with an accessible error message. Reload or remount to retry.
If mounting allocates resources before throwing, clean them up in the mount function: Filament has not received an instance to dispose yet.
Customizing the Blade view
Widgets using HasJsRenderer use filament-widgets::js-widget by default. To use a custom Blade view instead, set $view in your widget class:
protected string $view = 'filament.widgets.revenue-overview';The custom view replaces the default output; it does not automatically mount the JavaScript renderer. To keep the renderer, include the shared view in resources/views/filament/widgets/revenue-overview.blade.php:
@include('filament-widgets::js-widget')The shared view already includes the widget and section wrappers. Do not wrap this include in another <x-filament-widgets::widget> or <x-filament::section>. If you replace it entirely, your custom view is responsible for the widget layout and any renderer integration.
Building reusable plugin widgets
For a plugin, build the renderer as an ES module and register it as a published JavaScript asset, rather than using the host application's Vite manifest:
use Filament\Support\Assets\Js;
use Filament\Support\Facades\FilamentAsset;
FilamentAsset::register([
Js::make('revenue-overview', __DIR__ . '/../dist/revenue-overview.js')->loadedOnRequest(),
], 'acme/reports');Return the published URL from the widget:
use Filament\Support\Facades\FilamentAsset;
public function getRenderer(): string
{
return FilamentAsset::getScriptSrc('revenue-overview', 'acme/reports');
}Use php artisan filament:assets to publish the asset. Keep the module's default export and publish any imported chunks and styles too. You may alternatively return Filament\Support\RawJs containing a mount function; only use trusted JavaScript, never interpolate user input into it.
--- METRICS ---
- Files Extracted: 4
- Estimated Token Budget: ~14960 tokens
- Canonical Reference: https://codewiki.google/github.com/filamentphp/widgets