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 Tel Input Laravel Package

jojostx/filament-tel-input

Filament form field for international phone numbers, powered by intl-tel-input. Adds a searchable country dropdown with flags and dial codes, automatic number formatting, and validation so users enter phone numbers in the correct international format.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish assets

composer require jojostx/filament-tel-input
php artisan filament:assets
php artisan optimize:clear

Filament v3/v4 serves package JS and CSS out of public/, so filament:assets is not optional — skip it and the field silently renders as a plain text input with no country picker. Re-run it after every composer update of the package, and include it in your deploy script.

2. Register the plugin (if there is one)

Check vendor/jojostx/filament-tel-input/src/ for a *Plugin.php class. If present, add it to your panel provider:

// app/Providers/Filament/AdminPanelProvider.php
public function panel(Panel $panel): Panel
{
    return $panel
        // ...
        ->plugin(\JojoStx\FilamentTelInput\FilamentTelInputPlugin::make());
}

If there is no plugin class, it's a field-only package — nothing to register, just import the component. With a package this young, read src/ before trusting the README; the field class (Forms/Components/TelInput.php) is the real API surface, and the method list on it is the definitive one.

3. Where to look first

  • src/Forms/Components/TelInput.php — every option you can set.
  • resources/js/ — which intl-tel-input version is bundled and which options are forwarded to it. This matters more than anything else (see Gotchas).
  • dist/ — whether the built asset is committed or whether you need a npm run build in the package.

4. First use case

Add it to a UserResource or ContactResource form:

use JojoStx\FilamentTelInput\Forms\Components\TelInput;

TelInput::make('phone')
    ->label('Phone number')
    ->defaultCountry('US')
    ->required()
    ->tel()
    ->maxLength(255);

Then do the one test that matters: type a number, pick a country, save, and inspect what landed in the database. If it's +14155552671, the JS→Livewire sync works in your stack. If it's the raw typed digits, you'll need the normalization patterns below.

Implementation Patterns

Store E.164, display national. The widget's value is whatever the browser shows; never trust that as your storage format. Normalize on the way out:

TelInput::make('phone')
    ->defaultCountry('US')
    ->dehydrateStateUsing(fn (?string $state) => $state
        ? phone($state, 'US')->formatE164()
        : null)
    ->formatStateUsing(fn (?string $state) => $state
        ? phone($state)->formatNational()
        : null);

This uses propaganistas/laravel-phone (which wraps giggsey/libphonenumber-for-php). Do the same formatting in table columns:

TextColumn::make('phone')
    ->formatStateUsing(fn (?string $state) => $state
        ? phone($state)->formatInternational()
        : '—')
    ->searchable()
    ->copyable();

Validate with a real phone rule, not ->tel(). Laravel's tel rule is a loose regex and will happily accept garbage. Pair the field with a libphonenumber rule so validation agrees with what the widget's country selector implies:

use Propaganistas\LaravelPhone\Rules\Phone;

TelInput::make('phone')
    ->defaultCountry('US')
    ->rules([(new Phone())->country('US')]);

Centralize defaults in your own field subclass. Every form in the app wants the same defaultCountry, preferredCountries, and normalization. Wrap it once:

namespace App\Forms\Components;

use JojoStx\FilamentTelInput\Forms\Components\TelInput;

class PhoneInput extends TelInput
{
    protected function setUp(): void
    {
        parent::setUp();

        $this->defaultCountry(config('app.phone_country', 'US'))
            ->preferredCountries(['US', 'GB', 'CA', 'AU'])
            ->tel()
            ->dehydrateStateUsing(fn ($state) => $state
                ? phone($state)->formatE164()
                : null);
    }
}

Now a package swap (or a fork) is a one-file change instead of a hundred resource edits.

Make the default country tenant/context aware. Multi-tenant apps need the picker to open on the tenant's country:

->defaultCountry(fn () => filament()->getTenant()?->country_code ?? 'US')

Confirm the method accepts a closure — if it doesn't, set it in mutateFormDataBeforeCreate or via the field's afterStateHydrated.

Downstream integration. Once numbers are E.164, Twilio/Vonage/SMS and tel: links just work. Add a country_code derived column if you need to segment or filter by region:

TextColumn::make('phone_country')
    ->state(fn ($record) => phone($record->phone)->country())
    ->badge();

Gotchas and Tips

Livewire state is the number one failure mode. intl-tel-input can write to a hidden sibling input or reformat the visible one. If the value Livewire sees is stale, validation runs against the wrong string. Symptom: the form shows a valid number but ->required() fails or the DB gets raw digits. Fix by dispatching a native input event on the bound element after the plugin updates (el.dispatchEvent(new Event('input', { bubbles: true }))) or by calling $wire.set('data.phone', iti.getNumber()) in the widget's change handler.

Version mismatch with intl-tel-input. The library renamed options between majors (initialCountry → defaultCountry, and utils loading changed). If a chained method on the Filament field appears to do nothing, check resources/js/ for which version is actually bundled and whether the field forwards that option name. This is the most likely source of "the README says it supports X" bugs.

Repeaters and modals break selector-based init. A plugin that grabs document.querySelector('.iti') attaches to the first match only. In a Repeater, Builder, or a modal-opened form, every row after the first will be unstyled or won't sync. Verify $el-scoped Alpine init inside the component. If it's not scoped, this package won't survive a repeater without patching.

wire:navigate and SPA-style navigation. Scripts registered once at page load may not re-run after a Livewire navigate, leaving the field dead on the second page visit. The robust fix is to load the asset through Filament's FilamentAsset/@assets mechanism rather than a raw <script> in a Blade view, and to initialize in x-init on the component root rather than on DOMContentLoaded.

separateDialCode(true) changes what the input holds. The visible input may contain only the national portion. If you dehydrate from the visible input you'll save 4155552671 and lose the country. Always pull the full number from the plugin's getNumber() (E.164) rather than the DOM value — and re-test after toggling this option.

Tests can't exercise the JS. In Pest/PHPUnit Livewire tests the widget never runs, so fillForm(['phone' => '+14155552671']) is the correct approach — write your assertions against E.164 input and let the dehydrate hook be covered by a unit test on the phone helper instead.

Dark mode and custom themes. If you use a custom Filament theme via ->viteTheme(), package CSS may need to be imported into your theme.css, otherwise the dropdown renders unstyled in dark mode. Check whether the package's styles are registered through FilamentAsset::register([...]) or only shipped as a Blade @push.

Maturity risk — pin it. Three stars, zero dependents, single maintainer. Composer-lock the exact version, and read the field class before relying on any behavior. Keep the PhoneInput wrapper subclass described above so you can fork the package into packages/ (with a VCS repository entry) without touching feature code. The realistic alternatives are saade/filament-tel-input or a ~40-line Alpine component around intl-tel-input you own outright — for a package this small, the DIY route is often cheaper than debugging someone else's JS init.

Deploy checklist: composer install --no-dev → php artisan filament:assets → php artisan optimize:clear. Missing step two is the most common "it works locally" report for Filament asset-registering packages.

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.
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk