Skip to content
Devix Open Source

Frontend

Blade component

<x-phone-input
    name="phone"
    :value="old('phone', $user->phone)"
    country="AE"
    :preferred="['AE', 'SA', 'PK']"
    only="AE,SA,PK,GB,US"
    types="mobile"
    required
/>
Attribute Default
name — Input name. The widget also posts <name>_country.
value old(name) String or PhoneNumber. E.164 picks the right country.
country phone.default_country, else detected Initial country
preferred phone.input.preferred Pinned to the top of the list
only all Restrict the list
types any Live validation for these types, e.g. mobile
country-name <name>_country Name of the hidden country input
locale app locale Country names and messages
theme auto light, dark or auto
validate-on blur blur, input or off
dial-code separate inline keeps +971 inside the text

Any other attribute (id, class, placeholder, required, wire:model, aria-*) goes on the <input>.

What it renders

A real <input type="tel" autocomplete="tel"> with the number already formatted, so the form works before JavaScript loads — or without it. The widget then mounts on it. Browser messages come from the package's translations, so they match the server's wording in the user's language.

Styling

The field is Phone Input, so everything in its styling guide applies: CSS variables, classNames, the @layer stylesheet, and dark mode that follows your page's .dark or data-theme.

CSS variables in your app's stylesheet:

.dxp, .dxp-panel {
  --dxp-accent: #4f46e5;
  --dxp-radius: 6px;
  --dxp-height: 40px;
}

Classes on every part: once for all fields in config/phone.php, or per field:

'input' => [
    'class_names' => ['root' => 'shadow-sm', 'panel' => 'rounded-xl shadow-xl'],
],
<x-phone-input name="phone" class="font-mono" :widget="['classNames' => ['panel' => 'ring-1 ring-black/5'], 'flags' => 'emoji']" />

class and any other HTML attribute go on the <input>. widget takes any Phone Input option, merged over the component's own.

Keep your own field: if your app already styles every input (Tailwind Forms, a design system, Filament), let the widget float its country button inside your input instead of wrapping it:

<x-phone-input name="phone" class="form-input w-full" :widget="['layout' => 'overlay']" />

The input keeps its classes, borders and focus ring; only the country button and the dropdown come from the widget. See the overlay layout.

The markup itself:

php artisan vendor:publish --tag=phone-views

This copies the component to resources/views/vendor/phone/components/phone-input.blade.php, where you can wrap it, add a label and error slot, or change how assets load.

Assets

By default the component loads Phone Input from the Devix CDN, pinned to the release this package was tested with, once per page. To bundle it yourself instead:

PHONE_ASSETS_URL=
import { createPhoneInput } from '@devix-labs/phone-input';
import '@devix-labs/phone-input/styles.css';

document.querySelectorAll('input[data-devix-phone]').forEach((input) =>
  createPhoneInput(input, JSON.parse(input.dataset.devixPhone)),
);

Livewire

The field is wrapped in wire:ignore, so Livewire's DOM updates don't reset the widget. Fields added later — Livewire, Turbo, modals — are mounted automatically.

wire:model is bound to a hidden input that receives the full number: E.164 once the number is complete, and the typed text with its calling code before that (+92 30). The rule needs no country field either way, and an unfinished number gets a useful message (too short):

<x-phone-input name="whatsapp" country="PK" wire:model.live="whatsapp" />
public string $whatsapp = '';

protected function rules(): array
{
    return ['whatsapp' => ['required', Phone::rule()->mobile()]];
}

Inertia, React and Vue

Use the widget's own wrapper and send phone and phone_country with the form. The same rule validates it:

import { PhoneInput } from '@devix-labs/phone-input/react';

<PhoneInput
  value={data.phone}
  initialCountry="AE"
  onChange={(e164, result) => setData({ ...data, phone: e164 ?? '', phone_country: result.region ?? '' })}
/>

Posting E.164 means the country field is optional. Send it anyway, so an unfinished number is still read with the right country and the error explains what's wrong.

Updated 15 Sep 2026