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

Filament Sketchpad Laravel Package

valentin-morice/filament-sketchpad

A Filament form field that adds a sketchpad canvas for drawing or capturing signatures with mouse and touch. Save sketches as images, customize brush and canvas settings, and integrate quickly into Laravel Filament forms and resources.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register the field

composer require valentin-morice/filament-sketchpad

The package ships a single form component (ValentinMorice\FilamentSketchpad\Forms\Components\Sketchpad) that renders an HTML canvas and pushes the drawing into Livewire state as a base64 PNG data URL (data:image/png;base64,...). There is no panel plugin to register — you use it like any other Filament field.

First steps, in order

  1. Read the README on the repo for the exact fluent API (height(), width(), penColor(), backgroundColor(), clearButton() — names vary by release; confirm against src/Forms/Components/Sketchpad.php).
  2. Add a migration column. A base64 PNG of a signature is typically 15–80 KB of text, so use longText, not string:
$table->longText('signature')->nullable();
  1. Cast/allow it on the model:
protected $fillable = ['signature'];
  1. Drop it into a resource form:
use ValentinMorice\FilamentSketchpad\Forms\Components\Sketchpad;

Sketchpad::make('signature')
    ->label('Customer signature')
    ->required();

First real use case: signature capture on a contract/consent form, or a quick annotation on a job/ticket record. That is where this package pays for itself — you avoid pulling in a full JS canvas dependency and get a Filament-native field in ~3 lines.

Implementation Patterns

Pattern 1 — Store the raw data URL (simplest)

Keep the base64 string in the column and decode at render time. This is the path of least resistance and what the package is designed around.

Sketchpad::make('signature')
    ->required()
    ->dehydrated(); // default

Render it read-only with a custom view or an ImageColumn/ViewField that unwraps the prefix. In an infolist:

use Filament\Infolists\Components\ImageEntry;

ImageEntry::make('signature')
    ->getStateUsing(fn ($record) => $record->signature) // data URL works in <img src> as-is
    ->height(120);

Because a data: URI is valid in <img src>, you often do not need to touch the filesystem at all.

Pattern 2 — Decode and persist to disk

If you need a real file (PDF embedding, S3, virus scanning, signed URLs), intercept the state on save:

Sketchpad::make('signature')
    ->dehydrateStateUsing(function (?string $state) {
        if (blank($state)) {
            return null;
        }

        $data = preg_replace('#^data:image/\w+;base64,#i', '', $state);
        $path = 'signatures/' . uuid_create() . '.png';

        Storage::disk('private')->put($path, base64_decode($data));

        return $path; // store the path, not the blob
    });

Then expose it on the model:

public function signatureUrl(): ?string
{
    return $this->signature
        ? Storage::disk('private')->temporaryUrl($this->signature, now()->addMinutes(10))
        : null;
}

Pattern 3 — Validation and guards

Base64 payloads can be spoofed, so validate size/shape server-side rather than trusting the widget:

Sketchpad::make('signature')
    ->rules([
        'nullable',
        'string',
        'max:1048576', // ~1MB of base64 text
        function (string $attribute, $value, $fail) {
            if ($value && ! preg_match('#^data:image/png;base64,#', $value)) {
                $fail('Invalid signature payload.');
            }
        },
    ]);

Do the same on the model/observer if records can be created outside Filament.

Pattern 4 — Repeater / multi-signature workflows

For "sign off each line item" forms, put the field inside a Repeater and give each item a stable key so Livewire doesn't reuse canvas state between rows:

Repeater::make('signoffs')
    ->schema([
        TextInput::make('name')->required(),
        Sketchpad::make('signature')->required(),
    ])
    ->collapsible()
    ->itemLabel(fn (array $state): ?string => $state['name'] ?? null);

Verify on your Filament version that the canvas re-initialises on add/delete of rows — that is the most common bug class with canvas fields in repeaters, so smoke-test it before shipping.

Pattern 5 — Editing existing records

On EditRecord, the field must hydrate from the stored data URL (Pattern 1) or from a fetched file (Pattern 2). For Pattern 2 you need a formatStateUsing that converts the path back into a data URL:

->formatStateUsing(fn (?string $state) => $state
    ? 'data:image/png;base64,' . base64_encode(Storage::disk('private')->get($state))
    : null
)

If the field cannot render an existing value, users will silently overwrite signatures on every save — treat this as the acceptance test for your integration.

Pattern 6 — Output to PDF / notifications

Once you have a path (Pattern 2), embedding into a DomPDF/mPDF contract is trivial:

$signature = Storage::disk('private')->path($record->signature);
// <img src="{{ $signature }}"> or $pdf->image($signature, ...)

Attach to a notification with ->attachData(base64_decode($data), 'signature.png', ['mime' => 'image/png']) if you stayed on Pattern 1.

Gotchas and Tips

  • State is a data URL, not a TemporaryUploadedFile. Do not reach for FileUpload helpers (saveUploadedFileUsing, disk(), directory()) unless the README explicitly lists them. Decode with preg_replace('#^data:image/\w+;base64,#', '', $state) and base64_decode().
  • string columns truncate silently. A drawn signature can exceed 255 chars easily. Use longText/text. If you see a blank signature after save, check the column type before blaming the JS.
  • Request/DB size limits. Base64 inflates by ~33%. A 100 KB PNG becomes ~133 KB of text and may push request bodies over post_max_size on multi-signature forms. Cap canvas dimensions and pen width.
  • Livewire re-renders can clear the canvas. Any sibling field that triggers a full component re-render (e.g. a Select that toggles field visibility) risks wiping an in-progress drawing. Wrap dependent sections so the sketchpad isn't re-mounted, or test with ->live() toggles explicitly.
  • Modal and repeater hydration are the weak spots. Test: open modal → draw → cancel → reopen (state should be empty); edit record with existing signature (should render); add repeater row → draw → add another row (should not copy). These are the bugs you will actually ship.
  • required() on an empty canvas. The field starts blank and may dehydrate as null or ''. Confirm which, because required + nullable behave differently and can block submission with no visible feedback.
  • Validation is client-trust by default. Nothing stops a crafted Livewire request from posting arbitrary text into the column. Add a rules() closure (Pattern 3) or a model mutator/observer that rejects non-PNG data URLs.
  • Clearing state. If you add a "Clear" button or a ->dehydrated(false) toggle, verify the stored value is actually nulled rather than leaving stale base64 on the record.
  • Dark mode and background. A transparent PNG drawn on a dark Filament theme can look blank in an <img>. Set an explicit backgroundColor() (white) at capture time so exports/PDFs are readable.
  • Testing: use Livewire::test(YourForm::class)->fillForm(['data' => ['signature' => $fixtureDataUrl]])->call('save') with a small hard-coded data URL fixture. You cannot simulate drawing in PHPUnit, so assert persistence/decoding logic, not the canvas.
  • Extension points are thin. This is a small, single-purpose package (18 stars, no dependents) — expect to subclass the field or publish its Blade view for anything custom (extra toolbar buttons, undo/redo, image background). Check php artisan vendor:publish tags before forking.
  • Pin the version. With no dependents and a solo maintainer, lock to a known-good tag and note it in your composer.json constraints; a Filament major bump may break field API compatibility without a corresponding release.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource