ysfkaya/filament-phone-input
A Filament 3 form field for international phone numbers, with country code selection, built-in validation, default country support, and configurable display and input formatting. Requires PHP 8.1+ and integrates directly into Filament forms.
Install in three commands, then check the branch matches your Filament major.
composer require ysfkaya/filament-phone-input
php artisan filament:assets
php artisan filament-phone-input:install
The filament-phone-input:install command publishes the package's config/translations and (alongside filament:assets) the compiled intl-tel-input JS/CSS into public/. Both must run on every environment that renders the panel — locally, in CI, and in your deploy script. If you skip them the field degrades to a bare text input with no flags, dropdown, or formatting, and there is no error message telling you why.
Before writing any code, confirm which major you installed. The README carries a note that the 3.x branch is what you want for Filament 3 panels, while main targets the newer major. Since the package is a thin wrapper over Filament form/table/infolist components, a mismatched major fails loudly at class-resolution time — check composer show ysfkaya/filament-phone-input against your filament/filament constraint first, not after debugging a blank field.
Where to look first: the "Quick Preview" block in the README is effectively the complete public API — every fluent method on the component in one list. Skim it once, then treat it as your reference. The Troubleshooting section (one entry, about NumberParseException) and the Upgrade section are the other two places worth reading before you start, because both describe failure modes you will hit on real data.
First use case — swap your existing TextInput: this is a one-line change in a Filament resource and immediately gives you country dropdown, per-country masking, and E.164 normalization on save.
// Form
PhoneInput::make('phone')->required(),
// Table
PhoneColumn::make('phone')->displayFormat(PhoneInputNumberType::NATIONAL),
// Infolist
PhoneEntry::make('phone')->displayFormat(PhoneInputNumberType::NATIONAL),
Sanity-check the first record you save: open the DB row. The default inputNumberFormat is E164, so 0555 123 45 67 typed in a Turkish locale lands as +905551234567. Confirm that before you build anything downstream — every other decision (uniqueness, search, API payloads, SMS dispatch) depends on the stored shape.
Worth knowing up front: this package includes propaganistas/laravel-phone as a dependency, so you already have PhoneNumber casting and validation available on the model side without a second composer require.
The three-surface triple. The package ships form, table, and infolist components that share one formatting enum. Use all three together so a number looks identical in the edit form, the index table, and the view page. PhoneInputNumberType::{E164, INTERNATIONAL, NATIONAL, RFC3966} is the same enum everywhere, which is the main reason to reach for PhoneColumn/PhoneEntry instead of a plain TextColumn with a format string.
Separate storage of dial code — the decision you'll make most often. Use countryStatePath('phone_country') when a downstream consumer needs the country independently of the number: SMS gateways that bill per-country, routing logic, or a legacy schema with a phone_country column. Pair it with ->countryColumn('phone_country') on the column and entry, otherwise the display component has no idea which country to parse against and falls back to guessing.
PhoneInput::make('phone')->countryStatePath('phone_country'),
PhoneColumn::make('phone')->countryColumn('phone_country'),
PhoneEntry::make('phone')->countryColumn('phone_country'),
If nothing downstream needs the split, don't split it. E.164 in a single column is unambiguous, indexes well, and makes ->unique() and ->searchable() behave.
Validation belongs on the field, not the model. validateFor(country: 'AUTO', type: libPhoneNumberType::MOBILE) gives you libphonenumber-grade validation in the form layer, which produces field-level error messages instead of 500s. Use the explicit array form (['US', 'GB']) when you know your markets — 'AUTO' is convenient but lets a user submit a valid number for a country you don't operate in. The lenient: true flag is your escape hatch for numbers that are plausible but not strictly valid (test data, ported legacy rows).
Set every format explicitly in one base class. inputNumberFormat (what's persisted), displayNumberFormat (read-only views), focusNumberFormat (what appears when the input is focused), and placeholderNumberType (the example number shown as placeholder) all default independently. Rather than repeat four fluent calls across thirty resources, subclass and set house defaults once:
class PhoneInput extends \Ysfkaya\FilamentPhoneInput\Forms\PhoneInput
{
protected function setUp(): void
{
parent::setUp();
$this->defaultCountry(config('app.phone_country', 'US'))
->inputNumberFormat(PhoneInputNumberType::E164)
->displayNumberFormat(PhoneInputNumberType::NATIONAL)
->focusNumberFormat(PhoneInputNumberType::E164);
}
}
This also gives you one place to swap the package out later — which matters, given it's a 329-star single-maintainer package with no dependents driving its stability.
Constrain the country list per market. onlyCountries(['tr','us','gb']) for a single-market panel, excludeCountries([...]) for a global one that just needs to hide sanctioned/unsupported regions, countryOrder(['tr']) to pin your home market to the top of a long list, and countrySearch() once the list gets long enough to scroll. These accept closures, so a market selected elsewhere in the form can drive them.
Geo IP lookup — keep it, but make it yours. The component does a geo lookup on mount by default to guess the initial country. That's an outbound network call on every form render. For anything at scale, supply ipLookup() with a cached/header-derived resolution, and disableLookup() in local dev and test suites where the call will time out or leak IPs to a third party.
customOptions() is the escape valve. The wrapper doesn't cover every intl-tel-input option. Rather than fork, pass the raw option through customOptions(['foo' => true]) and let the JS layer handle it.
NumberParseException / "Number requires a country to be specified" is almost always legacy data with no area code, not a bug. Set ->defaultCountry('US') and the parser has something to work with. The README is honest that there's no way to infer a country from a bare digits string — plan a backfill for existing rows.validateFor() emits Laravel-Phone's messages, which expect an entry in lang/*/validation.php. Without it users see a raw translation key. Add it in the same PR as the validation call.showFlags(false) silently misbehaves unless combined with separateDialCode or disallowDropdown — the flag is what communicates the selected country when there's a dropdown. The README warns about this; it's easy to miss in review.inputNumberFormat E164, displayNumberFormat NATIONAL) looks inconsistent until you remember one is storage and one is presentation. Do not "fix" it by switching input to NATIONAL: you'll lose unambiguous storage, break ->unique(), and make every API payload locale-dependent.intl-tel-input speaks lowercase ISO2 (tr), propaganistas/laravel-phone expects uppercase (TR). Normalize at every boundary — validation, PhoneNumber casts, queue jobs that re-parse the stored value. Verify what countryStatePath actually writes in your version rather than assuming.onlyCountries/initialCountry from $get() works on initial mount, but intl-tel-input initializes once; a Livewire re-render may not re-init the dropdown. If you need the widget to rebuild, force it with ->key() on the component. Test this in the browser, not in a Livewire test — assertions on fillForm() bypass the JS entirely.containerClass(...), while the Usage section documents customContainer(...). Check the method's actual presence in the trait for your installed version before copying either name into a base class.public/ for the published assets and re-run php artisan filament:assets after any upgrade; second, open the browser console for intl-tel-input initialization errors; third, inspect the hidden input the widget maintains alongside the visible one — that hidden field is what actually reaches Livewire state, so Livewire devtools will show you the real submitted value.PhoneColumn::displayFormat() only changes rendering. A searchable() column of E.164 values won't match a user typing 0555, and sortable() sorts lexicographically by + prefix. If phone search matters, plan a normalized search column or an accessor rather than relying on the displayed format.PhoneInput subclass (house defaults), a custom ipLookup closure (cached country resolution), and customOptions (raw JS options). Avoid editing published assets — they're regenerated by filament:assets and your changes disappear on the next deploy.How can I help you explore Laravel packages today?