jeffersongoncalves/filament-qrcode-field
A Filament QR code field that adds a scannable QR input component using html5-qrcode, with publishable config for modal width, reader size and scanner FPS. Supports Filament 3, 4 and 5 through matching package versions.
1. Match the package major to your Filament major, then install.
| Package | Filament |
|---|---|
1.x |
3.x |
2.x |
4.x |
3.x |
5.x |
# Filament 5 project
composer require jeffersongoncalves/filament-qrcode-field:^3.0
There is no install command, no migration, no panel plugin registration — the field is auto-discovered. Requires PHP 8.2+.
2. Publish the config (do this on day one, not later).
php artisan vendor:publish --tag="filament-qrcode-field-config"
You get config/filament-qrcode-field.php:
'asset_js' => 'https://unpkg.com/[email protected]/html5-qrcode.min.js',
'modal' => ['width' => Width::Large],
'reader' => ['width' => '600px', 'height' => '600px'],
'scanner' => ['fps' => 10, 'width' => 250, 'height' => 250],
3. Drop it into a form and try it on a phone.
use JeffersonGoncalves\Filament\QrCodeField\Forms\Components\QrCodeInput;
QrCodeInput::make('qrcode')
->label('Asset QR code')
->required(),
That's the whole first use case: an inventory/asset-tag form where a warehouse user taps the scan button, the camera modal opens, and the decoded string lands in the text input. Because it is still an ordinary Filament text input underneath, users without a camera can type or paste the value, and USB/keyboard-wedge barcode guns work with zero extra code.
Where to look first when something misbehaves: browser devtools Network tab (is html5-qrcode.min.js loading from the CDN?) and the Console (camera permission / getUserMedia errors). Nearly all failures live in the browser, not in PHP.
Scan-to-lookup (the highest-value pattern). Scan a code, then redirect to the record. Keep the scanned field out of the persisted model by making it a dehydrated(false) field on a small dedicated form page, or a table header action:
QrCodeInput::make('scan')
->label('Scan to open')
->dehydrated(false)
->live()
->afterStateUpdated(function (?string $state, $livewire) {
if (blank($state)) {
return;
}
$asset = Asset::where('qr_code', trim($state))->first();
if (! $asset) {
Notification::make()->title('Unknown code')->danger()->send();
return;
}
$livewire->redirect(AssetResource::getUrl('edit', ['record' => $asset]));
}),
Normalize before it hits your database. QR payloads are frequently wrapped (ASSET:123, a full URL, a JSON blob, trailing newline from the encoder). Do the parsing in the field, not in ten places in your app:
QrCodeInput::make('qr_code')
->dehydrateStateUsing(fn (?string $state) => Str::of($state ?? '')
->trim()
->afterLast(':') // ASSET:123 -> 123
->toString())
->rule('regex:/^[A-Z0-9-]{6,32}$/'),
Pair scanning with uniqueness/exists validation so a duplicate scan fails fast instead of creating a second row:
QrCodeInput::make('serial')
->required()
->unique(ignoreRecord: true)
->maxLength(255),
Reactive forms. ->live() on the QR field is what makes downstream fields update (e.g. scanning a product code auto-fills product_id, name, price via afterStateUpdated). Without ->live() the state is only read at submit time, which is fine for plain capture but not for "scan and preview".
Inside table actions / modals. It works in an action's form schema too — useful for a "Scan out" button on an inventory table row's page, but see the modal-in-modal note in Gotchas.
Server-side handling is just a string. Store it as a normal string/text column, index it if you look assets up by it, and validate shape server-side regardless of what the camera produced — the client-side scanner is a convenience, not a trust boundary.
Pin the exact minor, and check the Filament floor. 3.0.4 bumped the requirement to filament/filament:^5.3 because of Page::routes() signature changes. On a Filament 5.0–5.2 app, composer require ...:^3.0 can resolve to a version you can't run. Pin ^3.0.3 or upgrade Filament; don't assume "5.x" in the README means any 5.x.
Camera APIs need a secure context. getUserMedia works on https:// or localhost only. Testing on http://192.168.1.x:8000 (a very common artisan serve --host=0.0.0.0 setup on a phone) will silently fail to open the camera. Use a tunnel, a self-signed cert, or Filament's Herd/Valet TLS domain.
The JS comes from a CDN by default. That means: it breaks on air-gapped/offline deployments, it can trip a strict Content-Security-Policy, and you inherit unpkg's availability. Fix by downloading html5-qrcode.min.js into public/ and pointing asset_js at it:
'asset_js' => '/vendor/html5-qrcode/2.3.8/html5-qrcode.min.js',
Keep the version in the filename — the changelog explicitly moved away from an unversioned unpkg URL for this reason.
Config changes may not take effect. If you run php artisan config:cache in deploy, editing the config file locally does nothing until you re-cache. Same for Width::Large — it's a Filament enum, so import Filament\Support\Enums\Width if you switch to Width::Screen or similar, and check the modal isn't wider than the phone viewport.
reader vs scanner are different things. reader is the container box in the modal; scanner is the video/qrbox size and fps. Enlarging reader without touching scanner gives a big empty box around a 250px viewfinder. Raising fps above ~10–15 improves detection on small/dense codes but drains battery and heats phones — it's a real tradeoff on rugged handhelds.
You need >= 3.0.3 for reliable repeat scanning. The 3.0.3 release rewrote the JS from Html5QrcodeScanner to Html5Qrcode to fix: camera not reopening on the second scan, AbortError when closing the modal with the stream live, stale DOM on re-init, and poor QR-version-10 reliability. If users report "first scan works, second one shows a black box", you're on an old build.
One QR field per page/view is the safe assumption. The reader renders into a DOM element in a modal; two or more instances in the same Livewire component or nested modals is not something the package advertises testing for. If you need it, verify manually before shipping, and prefer separate actions over a grid of scan buttons.
Treat the decoded value as hostile input. A QR code is an attacker-controllable string. If you redirect to a scanned URL, restrict the host allowlist (open-redirect); if you fetch it server-side, you have an SSRF vector; if you render it, escape it. Never ->dehydrated() straight into a URL column without validation.
Debugging checklist: (1) Network → CDN 404/CSP block; (2) Console → NotAllowedError (permission denied) vs NotFoundError (no camera) vs NotReadableError (camera in use by another app/tab); (3) confirm the modal opened on top (Filament z-index); (4) confirm ->live() if dependent fields aren't reacting; (5) test on iOS Safari specifically — it's the most common divergence from desktop Chrome.
Localization is free. 18 locales ship in-box; publish with --tag="filament-qrcode-field-translations" if you want to reword the modal/scan-button copy.
Bus-factor awareness. ~20 stars, 0 dependents, single maintainer, no documented publishable Blade views. The escape hatch when you need to change modal markup or scan-button behavior is to subclass QrCodeInput, copy its view into your app, and point $view at your copy. Before doing that, budget an afternoon reading the component's Blade + inline JS — it's small, and that read is also the fastest way to answer "what does this actually support?" questions the README doesn't cover.
Automated browser tests need a fake camera. Dusk/Pest browser tests with Chrome need --use-fake-device-for-media-stream --use-fake-ui-for-media-stream; otherwise the permission prompt hangs the test. Unit-test the consequences (validation, normalization, lookup) rather than the scan itself.
How can I help you explore Laravel packages today?