pteal79 / signature-capture
A NativePHP Mobile UI component for capturing handwritten signatures as SVG
Package info
github.com/pteal79/signature-capture
Type:nativephp-ui-plugin
pkg:composer/pteal79/signature-capture
Requires
- php: ^8.2
- nativephp/mobile: ^4.0
Requires (Dev)
- pestphp/pest: ^3.0
README
A signature pad for NativePHP Mobile: a bounded area the user signs into with a finger or stylus, with Clear and Confirm controls. The signature comes back to PHP as an SVG document string.
Drawing is handled by a native renderer on each platform — Jetpack Compose on Android, SwiftUI on iOS — so strokes track the finger at display frame rate. PHP is only involved when a stroke ends or a control is pressed.
Installation
composer require pteal79/signature-capture
Then rebuild the app. A UI component's native half is generated at build
time, so a composer update alone gives you an element that serialises
fine and renders nothing:
php artisan native:run
Installing from a local path repository
Composer symlinks path repositories by default, and the app bundler
resolves symlinks (getRealPath()) before working out each file's place in
the archive. A symlinked plugin collapses to a single junk entry, so on
device the package simply isn't there — which surfaces as
Unknown native element type: signature_capture. Ask for a copy instead:
{
"type": "path",
"url": "../packages/pteal79/signature-capture",
"options": { "symlink": false }
}
Composer mirrors the directory wholesale, dev dependencies included, and
ignores archive.exclude when it does. So if you run this package's test
suite, delete its vendor/ afterwards or exclude it from the bundle in
config/nativephp.php:
'cleanup_exclude_files' => [ 'vendor/pteal79/signature-capture/vendor', 'vendor/pteal79/signature-capture/tests', ],
Because it's a copy, changes to the package reach the app only after
composer update pteal79/signature-capture.
Usage
<native:column class="p-4 gap-4"> <native:text class="text-lg font-semibold">Please sign below</native:text> <native:signature-capture class="w-full" placeholder="Sign here" on-confirm="storeSignature" on-clear="signatureCleared" /> </native:column>
use Native\Mobile\Edge\NativeComponent; class SignOffScreen extends NativeComponent { public function storeSignature(string $svg): void { $this->delivery->update(['signature' => $svg]); } public function signatureCleared(): void { // The user wiped the pad. } }
$svg is a complete SVG document — store it, render it in a PDF, or drop
it straight into a Blade view with {!! $svg !!}.
Building it programmatically
use Pteal79\SignatureCapture\Elements\SignatureCapture; SignatureCapture::make() ->placeholder('Sign here') ->strokeColor('#0F172A', dark: '#F8FAFC') ->strokeWidth(3) ->padHeight(240) ->confirmLabel('Accept') ->onConfirm('storeSignature') ->class('w-full');
What comes back
A single SVG document sized in density-independent units, with a viewBox
matching the pad — so it scales to any size without going fuzzy:
<svg xmlns="http://www.w3.org/2000/svg" width="343" height="200" viewBox="0 0 343 200"> <g fill="none" stroke="#111827" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <path d="M42.5 128 Q48.25 120.5 55.75 116.25 …"/> </g> </svg>
One <path> per stroke, smoothed with quadratic curves through the
midpoints of the captured samples. The on-screen drawing is built from the
same walk, so the exported SVG is exactly what the user saw.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
stroke-color / dark-stroke-color |
hex | neutral | Ink colour |
stroke-width |
float | 2.5 |
Ink width |
pad-color / dark-pad-color |
hex | white / near-black | Drawing surface fill |
border-color / dark-border-color |
hex | grey | Surface border, guide line and placeholder |
accent-color |
hex | #2563EB |
Confirm button fill |
pad-height |
float | 200 |
Height of the drawing area; 0 fills the space available |
corner-radius |
float | 12 |
Surface corner radius |
placeholder |
string | Sign here |
Hint shown while empty; "" shows nothing |
show-guide |
bool | true |
Draw the signing line across the pad |
controls |
bool | true |
Show the built-in Clear / Confirm buttons |
confirm-label |
string | Confirm |
|
clear-label |
string | Clear |
|
disabled |
bool | false |
Read-only pad, controls off |
clear-token |
string | — | Change the value to clear the pad from PHP |
a11y-label / a11y-hint |
string | Signature pad |
Screen-reader text |
Colours are hex strings (#RRGGBB or #AARRGGBB), the same spelling the
core elements take. Any colour left unset picks a neutral default for the
active colour scheme, so the component looks right in light and dark
without configuration.
Tailwind classes work as they do on any element — class="w-full" sizes
and styles the component as a whole.
Events
| Attribute | Fires | Receives |
|---|---|---|
on-confirm |
Confirm pressed | The signature as an SVG string |
on-clear |
Clear pressed | — |
on-change |
Each stroke ends, and on clear | The current SVG, or '' when empty |
Confirm is disabled while the pad is empty, so on-confirm never fires
with an empty signature.
@submit, @dismiss and @change are accepted as directive-style
aliases for on-confirm, on-clear and on-change. Other @-prefixed
spellings (@confirm, @clear) do not work: the template
precompiler only rewrites its own list of directive names and strips every
other @event= binding before an element sees it.
Supplying your own controls
Turn the built-in buttons off, track the signature through on-change,
and clear the pad by changing clear-token:
<native:signature-capture class="w-full" controls="false" on-change="signatureChanged" :clear-token="$clearToken" /> <native:row class="gap-2 mt-4"> <native:button @tap="clear" class="flex-1">Clear</native:button> <native:button @tap="save" class="flex-1" :disabled="$signature === ''">Save</native:button> </native:row>
public string $signature = ''; public int $clearToken = 0; public function signatureChanged(string $svg): void { $this->signature = $svg; } public function clear(): void { $this->clearToken++; } public function save(): void { // $this->signature holds the SVG. }
Any value works as a clear token as long as it differs from the last one
published. A PHP-driven clear doesn't fire on-clear — you already know
it happened — but it does fire on-change with an empty string.
Notes
- Filling the available height.
pad-height="0"makes the pad take whatever height is left over, which needs a bounded parent — give the element a height (class="h-full") or put it in a sized container. - Inside a scroll view. On Android the pad claims the touch, so a stroke won't be stolen by an enclosing scroll container. On iOS a signature pad inside a scrolling ancestor can compete with the scroll gesture; prefer a fixed, non-scrolling area for signing.
Testing
The element serialises like any other, so component tests can assert it:
use Native\Mobile\Testing\Native; Native::test(SignOffScreen::class) ->assertElement('signature_capture', fn ($node) => $node['props']['placeholder'] === 'Sign here');
The package's own suite covers the element's wire output:
composer test
License
MIT