saade/filament-fullcalendar
Filament FullCalendar
FullCalendar for Filament panels: show your models on a calendar and view, create, edit, drag and resize them with Filament actions.
Version compatibility
| Plugin | Filament | FullCalendar | Install |
|---|---|---|---|
| 4.x | 4.x, 5.x | 6.x | composer require saade/filament-fullcalendar:"^4.0" |
| 3.x | 3.x | 6.x | composer require saade/filament-fullcalendar:"^3.0" |
| 2.x | 2.x | 5.x | composer require saade/filament-fullcalendar:"^2.0" |
Upgrading from 3.x? Read the upgrade guide.
Table of contents
- Installation
- Usage
- Configuration
- Interacting with actions
- Intercepting events
- Controlling the calendar
- Render hooks
- Recipes
- Changelog
- Contributing
- Security Vulnerabilities
- Credits
- License
Installation
- Install the package via composer:
composer require saade/filament-fullcalendar:"^4.0"
- Add the plugin's styles to your panel's custom theme. If the panel does not have a custom theme yet, create one first by following the Filament docs.
@import '../../../../vendor/saade/filament-fullcalendar/resources/css/filament-fullcalendar.css';
@source '../../../../vendor/saade/filament-fullcalendar/resources/views/**/*.blade.php';
Then rebuild your assets with npm run build.
- Register the plugin on every panel that shows a calendar. The widget does not work on a panel without it.
use Filament\Panel;
use Saade\FilamentFullCalendar\FilamentFullCalendarPlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(FilamentFullCalendarPlugin::make());
}
Usage
- Create a widget and choose the Custom type when asked:
php artisan make:filament-widget CalendarWidget
- Make it extend
Saade\FilamentFullCalendar\Widgets\FullCalendarWidget, and remove the$viewproperty and the Blade view that the command generated:
<?php
namespace App\Filament\Widgets;
use Saade\FilamentFullCalendar\Widgets\FullCalendarWidget;
class CalendarWidget extends FullCalendarWidget
{
/**
* FullCalendar calls this whenever it needs events, such as when the
* user clicks prev/next or switches views.
*
* @param array{start: string, end: string, timezone: string} $info
*/
public function fetchEvents(array $info): array
{
return [];
}
}
Returning events
fetchEvents() returns an array of FullCalendar event objects. $info holds the visible range, so only the events that overlap it need to be loaded:
<?php
namespace App\Filament\Widgets;
use App\Models\Event;
use Saade\FilamentFullCalendar\Widgets\FullCalendarWidget;
class CalendarWidget extends FullCalendarWidget
{
public function fetchEvents(array $info): array
{
return Event::query()
->where('starts_at', '<', $info['end'])
->where('ends_at', '>', $info['start'])
->get()
->map(fn (Event $event): array => [
'id' => $event->id,
'title' => $event->name,
'start' => $event->starts_at,
'end' => $event->ends_at,
])
->all();
}
}
[!NOTE] Compare the range with an overlap test like the one above. Filtering with
starts_at >= startandends_at <= endhides every event that begins before or ends after the visible range.
[!NOTE] FullCalendar treats the
endof an event as exclusive. An all-day event that should cover October 6th to 8th needs2026-10-09as itsend.
The EventData class
Saade\FilamentFullCalendar\Data\EventData is a fluent way to build the same event objects:
use App\Filament\Resources\Events\EventResource;
use App\Models\Event;
use Saade\FilamentFullCalendar\Data\EventData;
public function fetchEvents(array $info): array
{
return Event::query()
->where('starts_at', '<', $info['end'])
->where('ends_at', '>', $info['start'])
->get()
->map(fn (Event $event): EventData => EventData::make()
->id($event->id)
->title($event->name)
->start($event->starts_at)
->end($event->ends_at))
->all();
}
| Method | Description |
|---|---|
id(int | string $id) |
Identifies the event. Required for the view, edit and delete actions to find the record. |
title(string $title) |
The text shown on the event. |
start(DateTimeInterface | string $start) |
When the event begins. |
end(DateTimeInterface | string | null $end) |
When the event ends (exclusive). |
allDay(bool $allDay = true) |
Shows the event in the all-day section, without a time. |
url(string $url, bool $shouldOpenUrlInNewTab = false) |
Visits a URL when the event is clicked, instead of opening the view action. |
backgroundColor(string $color), borderColor(string $color), textColor(string $color) |
Colors for this event. Any CSS color works. |
groupId(int | string $groupId) |
Events sharing a group are dragged and resized together. |
resourceId(int | string $resourceId), resourceIds(array $resourceIds) |
Associates the event with resources. |
extendedProps(array $props) |
Your own data, available to the render hooks as event.extendedProps. |
extraProperties(array $properties) |
Any other event property, such as display, classNames, editable or rrule. |
Showing the calendar on its own page
The widget can be used anywhere a Filament widget can. To give it a page of its own, create a custom page and return the widget from getHeaderWidgets():
<?php
namespace App\Filament\Pages;
use App\Filament\Widgets\CalendarWidget;
use Filament\Pages\Page;
class Calendar extends Page
{
protected function getHeaderWidgets(): array
{
return [
CalendarWidget::class,
];
}
}
Filament adds every discovered widget to the default dashboard. If your dashboard does not define its own getWidgets(), keep the calendar off it by registering your widgets explicitly on the panel with ->widgets([...]).
Configuration
Plugin methods
Options set on the plugin apply to every calendar in the panel:
use Saade\FilamentFullCalendar\FilamentFullCalendarPlugin;
$panel->plugin(
FilamentFullCalendarPlugin::make()
->selectable()
->editable()
->timezone('America/Sao_Paulo')
->locale('pt-br')
->plugins(['multiMonth'])
->config([
'firstDay' => 1,
]),
);
| Method | Default | Description |
|---|---|---|
selectable(bool $selectable = true) |
false |
Lets users click or drag over dates to create an event. See selectable. |
editable(bool $editable = true) |
false |
Lets users drag and resize events. See editable. |
timezone(string | Closure $timezone) |
config('app.timezone') |
The time zone dates are displayed in. See timeZone. |
locale(string | Closure $locale) |
The app locale | The language of the calendar. See locale. |
plugins(array $plugins, bool $merge = true) |
interaction, dayGrid, timeGrid, list, moment, momentTimezone |
FullCalendar plugins to enable. Pass false as the second argument to replace the defaults. |
schedulerLicenseKey(?string $key) |
null |
Your FullCalendar Premium license key. See Premium plugins and licensing. |
config(array $config) |
[] |
Any other FullCalendar option. |
Available plugins: interaction, dayGrid, timeGrid, list, multiMonth, rrule, moment, momentTimezone, and the premium scrollGrid, timeline, adaptive, resource, resourceDayGrid, resourceTimeline, resourceTimeGrid.
Configuring a single widget
Override config() on a widget to set FullCalendar options for that calendar only. It is merged over the plugin's config():
public function config(): array
{
return [
'initialView' => 'timeGridWeek',
'headerToolbar' => [
'left' => 'prev,next today',
'center' => 'title',
'right' => 'dayGridMonth,timeGridWeek,timeGridDay,listWeek',
],
'firstDay' => 1,
'slotMinTime' => '08:00:00',
'slotMaxTime' => '20:00:00',
];
}
The toolbar shows the title and the navigation buttons by default. Set headerToolbar as above to let users switch between views.
Options people ask about most often:
| Goal | Option |
|---|---|
| Choose which views the toolbar offers | headerToolbar, with view names such as dayGridMonth, timeGridWeek, timeGridDay, listWeek or multiMonthYear (needs the multiMonth plugin) |
| Choose the first view | initialView |
| Start the week on Monday | firstDay |
| Limit the events shown per day | dayMaxEvents |
| Stop users navigating or selecting outside a range, such as the past | validRange, selectConstraint |
| Highlight working hours | businessHours |
| 24-hour times | eventTimeFormat, slotLabelFormat |
config() is sent to the browser as JSON, so it cannot hold JavaScript functions. For event rendering callbacks, use the render hooks.
Premium plugins and licensing
The standard views (month, week, day, list, multi-month) are free and MIT licensed. The timeline, resource*, scrollGrid and adaptive plugins are part of FullCalendar Premium and need a license from FullCalendar, which is separate from this package. They are only enabled on a calendar when you add them to plugins().
FilamentFullCalendarPlugin::make()
->plugins(['resourceTimeline'])
->schedulerLicenseKey(config('services.fullcalendar.license_key'))
FullCalendar also publishes keys for evaluation, registered non-profits and open-source projects. Which one applies to you is defined by the FullCalendar license terms, so check them before going to production.
Interacting with actions
The calendar uses Filament Actions to view, create, edit and delete events. Tell the widget which model it works with and which fields the modals show:
<?php
namespace App\Filament\Widgets;
use App\Models\Event;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Grid;
use Illuminate\Database\Eloquent\Model;
use Saade\FilamentFullCalendar\Widgets\FullCalendarWidget;
class CalendarWidget extends FullCalendarWidget
{
public Model | string | null $model = Event::class;
public function getFormSchema(): array
{
return [
TextInput::make('name')
->required(),
Grid::make()
->schema([
DateTimePicker::make('starts_at')
->required(),
DateTimePicker::make('ends_at')
->required(),
]),
];
}
}
That is all it takes: a "New event" button appears above the calendar, clicking an event opens it, and the modal offers Edit and Delete. The form does not have to match the FullCalendar event object; add whichever fields your model has.
[!IMPORTANT] Each event returned from
fetchEvents()needs anidthat matches the model's key, so the actions can find the record.
Customizing actions
The actions are regular Filament actions, so they can be customized the same way. Override these methods to change them:
use Filament\Actions\Action;
use Saade\FilamentFullCalendar\Actions;
protected function headerActions(): array
{
return [
Actions\CreateAction::make(),
];
}
protected function modalActions(): array
{
return [
Actions\EditAction::make(),
Actions\DeleteAction::make(),
];
}
protected function viewAction(): Action
{
return Actions\ViewAction::make();
}
[!IMPORTANT] Use the actions from
Saade\FilamentFullCalendar\Actions, notFilament\Actions. They are wired to the widget's model, record and form schema.
Authorizing actions
The calendar does not check policies on its own. Any user who can see the widget can view, create, edit and delete the model's records through it, so authorize the actions that need it with authorize():
use Filament\Actions\Action;
use Saade\FilamentFullCalendar\Actions;
protected function headerActions(): array
{
return [
Actions\CreateAction::make()
->authorize('create'),
];
}
protected function modalActions(): array
{
return [
Actions\EditAction::make()
->authorize('update'),
Actions\DeleteAction::make()
->authorize('delete'),
];
}
protected function viewAction(): Action
{
return Actions\ViewAction::make()
->authorize('view');
}
Dragging and resizing open the edit action, and selecting dates opens the create action, so they follow the same rules.
Records are looked up through getEloquentQuery(), which is not scoped to the user or, in a panel with tenancy, to the current tenant. Override it to limit which records can be opened at all:
use Filament\Facades\Filament;
use Illuminate\Database\Eloquent\Builder;
protected function getEloquentQuery(): Builder
{
return parent::getEloquentQuery()->whereBelongsTo(Filament::getTenant());
}
Intercepting events
The widget has a method for each calendar interaction. Override one to change what it does, and call the parent to keep the default behavior:
| Method | Called when | Default |
|---|---|---|
onEventClick(array $event) |
An event is clicked | Opens the view action |
onEventDrop(array $event, array $oldEvent, array $relatedEvents, array $delta, ?array $oldResource, ?array $newResource) |
An event is dragged to another date | Opens the edit action |
onEventResize(array $event, array $oldEvent, array $relatedEvents, array $startDelta, array $endDelta) |
An event is resized | Opens the edit action |
onDateSelect(string $start, ?string $end, bool $allDay, ?array $view, ?array $resource) |
A date is clicked or a range is selected | Opens the create action |
onEventDrop() and onEventResize() return a boolean. Return true to move the event back to where it was.
Controlling the calendar
Call refreshRecords() on the widget to make the calendar fetch its events again. The built-in actions already do this after saving.
From any Livewire component or from JavaScript, dispatch these browser events:
| Event | Effect |
|---|---|
filament-fullcalendar--refresh |
Fetches the events again |
filament-fullcalendar--prev, filament-fullcalendar--next, filament-fullcalendar--today |
Navigates |
filament-fullcalendar--goto with date |
Moves to a date |
filament-fullcalendar--view with view |
Switches view |
$this->dispatch('filament-fullcalendar--goto', date: '2026-12-01');
$this->dispatch('filament-fullcalendar--view', view: 'timeGridWeek');
Render hooks
FullCalendar's event render hooks eventClassNames, eventContent, eventDidMount and eventWillUnmount are available as methods that return JavaScript:
public function eventDidMount(): string
{
return <<<'JS'
function ({ event, timeText, isStart, isEnd, isMirror, isPast, isFuture, isToday, el, view }) {
// Write your custom implementation here
}
JS;
}
Recipes
Filling the form from a date selection
Enable selectable(), then fill the create form with the selected dates:
use Filament\Schemas\Schema;
use Saade\FilamentFullCalendar\Actions;
protected function headerActions(): array
{
return [
Actions\CreateAction::make()
->mountUsing(function (Schema $schema, array $arguments): void {
$schema->fill([
'starts_at' => $arguments['start'] ?? null,
'ends_at' => $arguments['end'] ?? null,
]);
}),
];
}
Filling the form after dragging or resizing
Enable editable(). Dragging or resizing an event opens the edit action, which can be filled with the event's new dates:
use App\Models\Event;
use Filament\Schemas\Schema;
use Saade\FilamentFullCalendar\Actions;
protected function modalActions(): array
{
return [
Actions\EditAction::make()
->mountUsing(function (Event $record, Schema $schema, array $arguments): void {
$schema->fill([
...$record->attributesToArray(),
'starts_at' => $arguments['event']['start'] ?? $record->starts_at,
'ends_at' => $arguments['event']['end'] ?? $record->ends_at,
]);
}),
Actions\DeleteAction::make(),
];
}
Saving extra data when creating
use Saade\FilamentFullCalendar\Actions;
protected function headerActions(): array
{
return [
Actions\CreateAction::make()
->mutateDataUsing(fn (array $data): array => [
...$data,
'user_id' => auth()->id(),
]),
];
}
Coloring events
EventData::make()
->id($event->id)
->title($event->name)
->start($event->starts_at)
->end($event->ends_at)
->backgroundColor($event->status->isConfirmed() ? '#16a34a' : '#f59e0b')
->borderColor('transparent')
Event tooltip on hover
public function eventDidMount(): string
{
return <<<'JS'
function ({ event, el }) {
el.setAttribute('x-tooltip.raw', event.title)
}
JS;
}
Recurring events
Enable the rrule plugin and pass an rrule with the event:
EventData::make()
->id($event->id)
->title($event->name)
->extraProperties([
'rrule' => [
'freq' => 'weekly',
'byweekday' => ['mo', 'we'],
'dtstart' => '2026-10-05T10:00:00',
],
'duration' => '01:00',
])
Share your recipes
If you have a recipe to share, please open a PR and add it to this section.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.
Related Packages
A tree field for Filament: build and edit nested, hierarchical data such as menu...
A set of reusable Filament helpers, columns, fields, actions and more!
A set of reusable Filament helpers, columns, fields, actions and more!
Version History
| Version | Released | PHP | Laravel | License |
|---|---|---|---|---|
| v4.0.0 | ^8.2 | ^10.0| | MIT | |
| v3.2.4 | ^8.1 | ^10.0| | MIT | |
| v3.2.3 | ^8.1 | ^10.0| | MIT | |
| v3.2.2 | ^8.1 | ^10.0| | MIT | |
| v3.2.1 | ^8.1 | ^10.0| | MIT | |
| v3.2.0 | ^8.1 | ^10.0| | MIT | |
| v3.1.3 | ^8.0 | ^9.0| | MIT | |
| v3.1.2 | ^8.0 | ^9.0| | MIT | |
| v3.1.1 | ^8.0 | ^9.0| | MIT | |
| v3.1.0 | ^8.0 | ^9.0| | MIT | |
| v3.0.3 | ^8.0 | ^9.0| | MIT | |
| v3.0.2 | ^8.0 | ^9.0| | MIT | |
| v3.0.1 | ^8.0 | ^9.0| | MIT | |
| v3.0.0 | ^8.0 | ^9.0| | MIT | |
| v1.9.3 | ^8.0 | ^9.0| | MIT | |
| v1.9.2 | ^8.0 | ^9.0| | MIT | |
| v1.9.1 | ^8.0 | ^9.0| | MIT | |
| v1.9.0 | ^8.0 | ^9.0| | MIT | |
| v1.8.0 | ^8.0 | ^9.0| | MIT | |
| v1.7.1 | ^8.0 | ^9.0| | MIT | |
| v1.7.0 | ^8.0 | ^9.0| | MIT | |
| v1.6.0 | ^8.0 | ^8.73| | MIT | |
| v1.5.1 | ^8.0 | ^8.73| | MIT | |
| v1.5.0 | ^8.0 | ^8.73| | MIT | |
| v1.4.0 | ^8.0 | ^8.73| | MIT | |
| v1.3.1 | ^8.0 | ^8.73| | MIT | |
| v1.3.0 | ^8.0 | ^8.73| | MIT | |
| v1.2.1 | ^8.0 | ^8.73| | MIT | |
| v1.2.0 | ^8.0 | ^8.73| | MIT | |
| v1.1.0 | ^8.0 | ^8.73| | MIT |
Showing the latest 30 of 35. See every release on Packagist
Pre-releases (11)
| Version | Released | PHP | Laravel | License |
|---|---|---|---|---|
| v4.0.0-beta7 | ^8.2 | ^10.0| | MIT | |
| v4.0.0-beta6 | ^8.2 | ^10.0| | MIT | |
| v4.0.0-beta5 | ^8.2 | ^10.0| | MIT | |
| v4.0.0-beta4 | ^8.2 | ^10.0| | MIT | |
| v4.0.0-beta3 | ^8.2 | ^10.0| | MIT | |
| v4.0.0-beta2 | ^8.2 | ^10.0| | MIT | |
| v4.0.0-beta1 | ^8.2 | ^10.0| | MIT | |
| v3.0.0-beta4 | ^8.0 | ^9.0| | MIT | |
| v3.0.0-beta3 | ^8.0 | ^9.0| | MIT | |
| v3.0.0-beta2 | ^8.0 | ^9.0| | MIT | |
| v3.0.0-beta1 | ^8.0 | ^9.0| | MIT |