Scaffolding
Use the Artisan command to generate a new widget:
php artisan layup:make-widget BannerWidget
This creates two files:
app/Layup/Widgets/BannerWidget.php-- the widget classresources/views/components/layup/banner.blade.php-- the Blade view
Add --with-test to also generate a Pest test file:
php artisan layup:make-widget BannerWidget --with-test
Widget class structure
Every custom widget extends BaseWidget and implements two required methods:
<?php
declare(strict_types=1);
namespace App\Layup\Widgets;
use Crumbls\Layup\View\BaseWidget;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\TextInput;
class BannerWidget extends BaseWidget
{
public static function getType(): string
{
return 'banner';
}
public static function getLabel(): string
{
return 'Banner';
}
public static function getContentFormSchema(): array
{
return [
TextInput::make('title')
->label('Title')
->required(),
RichEditor::make('body')
->label('Body'),
TextInput::make('button_url')
->label('Button URL')
->url(),
];
}
public static function getDefaultData(): array
{
return [
'title' => 'Welcome',
'body' => '',
'button_url' => '',
];
}
public static function getPreview(array $data): string
{
return $data['title'] ?? '(empty)';
}
}
Required methods
getType(): string
A unique kebab-case identifier stored in JSON content. Must not collide with built-in types.
getLabel(): string
The display name shown in the widget picker.
Optional overrides
getContentFormSchema(): array
Return an array of Filament form components for the Content tab. This is where your widget's fields live.
getDefaultData(): array
Initial data for newly created instances of this widget.
getPreview(array $data): string
Short text shown on the builder canvas. The default implementation shows the first 60 characters of $data['content'], falls back to $data['label'], then $data['src'].
getIcon(): string
Heroicon name for the widget picker. Default: heroicon-o-puzzle-piece.
getCategory(): string
Category for grouping in the picker. Use one of: content, media, interactive, layout, advanced.
getSearchTerms(): array
Additional keywords for the widget picker search.
getValidationRules(): array
Laravel validation rules for widget data:
public static function getValidationRules(): array
{
return [
'title' => 'required|string|max:255',
'button_url' => 'nullable|url',
];
}
prepareForRender(array $data): array
Transform stored data before it reaches the Blade view. Use for computed values, URL resolution, or data formatting.
getAssets(): array
Declare JS/CSS dependencies:
public static function getAssets(): array
{
return [
'js' => ['https://cdn.example.com/lib.js'],
'css' => ['https://cdn.example.com/lib.css'],
];
}
Lifecycle hooks
Override these for side effects during the widget lifecycle:
// Called when widget is first added to a column
public static function onCreate(array $data, ?WidgetContext $context = null): array
{
return $data;
}
// Called when the editor slideover is saved
public static function onSave(array $data, ?WidgetContext $context = null): array
{
return $data;
}
// Called when the widget is deleted
public static function onDelete(array $data, ?WidgetContext $context = null): void
{
// Clean up uploaded files, etc.
}
// Called when the widget is duplicated
public static function onDuplicate(array $data, ?WidgetContext $context = null): array
{
// Copy files so the duplicate has its own copy
return $data;
}
The WidgetContext object provides:
$context->page; // ?Page model
$context->rowId; // ?string
$context->columnId; // ?string
$context->widgetId; // ?string
The Blade view
The view receives $data (the widget's data array) and $children (child components, if any):
@props(['data', 'children'])
<div>
<h2>{{ $data['title'] ?? '' }}</h2>
{!! $data['body'] ?? '' !!}
@if(!empty($data['button_url']))
<a href="{{ $data['button_url'] }}">Learn More</a>
@endif
</div>
View naming convention: resources/views/components/layup/{type}.blade.php where {type} matches getType().
Registration
Widgets in the App\Layup\Widgets namespace are auto-discovered. No config changes needed.
To use a different namespace, update config/layup.php:
'widget_discovery' => [
'namespace' => 'App\\Custom\\Widgets',
'directory' => app_path('Custom/Widgets'),
],
Or register explicitly in the widgets config array:
'widgets' => [
// ... built-in widgets
\App\Layup\Widgets\BannerWidget::class,
],
Or via the plugin:
LayupPlugin::make()
->widgets([
\App\Layup\Widgets\BannerWidget::class,
]),
Contributors
Thank you to everyone who has contributed to this package. Every pull request, bug report, and idea makes a difference.