PhoneInput

International phone number input with country selector

Usage

PhoneInput provides an international phone number input with country selector and formatting masks.

import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Your phone number"
      defaultCountry="us"
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}

Input features

PhoneInput component supports Input and Input.Wrapper component features and all input element props. The PhoneInput documentation does not include all features supported by the component – see the Input documentation to learn about all available features.

Input description

Variant
Size
Radius
import { PhoneInput } from '@mantine/core';
function Demo() {
  return (
    <PhoneInput
      label="Input label"
      description="Input description"
      placeholder="Your phone number"
      defaultCountry="us"
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}

Loading state

Set loading prop to display a loading indicator in the input:

import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Your phone number"
      defaultCountry="us"
      loading
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}

Controlled

Use value and onChange props to control the input value. The onChange callback receives the raw value and a details object containing the formatted value and country data.

Raw value: +1
Formatted: 
Country: –
import { useState } from 'react';
import { Code, PhoneInput, PhoneInputCountryData } from '@mantine/core';

function Demo() {
  const [value, setValue] = useState('+1');
  const [country, setCountry] = useState<PhoneInputCountryData | null>(null);
  const [formattedValue, setFormattedValue] = useState('');

  return (
    <>
      <PhoneInput
        label="Phone number"
        placeholder="Your phone number"
        defaultCountry="us"
        value={value}
        onChange={(rawValue, details) => {
          setValue(rawValue);
          setFormattedValue(details.formattedValue);
          setCountry(details.country);
        }}
        renderFlag={(c) => (
          <img
            src={`https://hatscripts.github.io/circle-flags/flags/${c?.iso2 ?? 'xx'}.svg`}
            alt={c?.name ?? 'Select country'}
            style={{ width: 20, height: 20, borderRadius: 20 }}
          />
        )}
      />
      <Code block mt="md">
        {[
          `Raw value: ${value}`,
          `Formatted: ${formattedValue}`,
          `Country: ${country?.iso2 ?? '–'}`,
        ].join('\n')}
      </Code>
    </>
  );
}

Preferred countries

Use preferredCountries prop to pin countries to the top of the dropdown.

import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Your phone number"
      defaultCountry="us"
      preferredCountries={['us', 'gb', 'ca']}
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}

Filter countries

Use onlyCountries to show only specific countries or excludeCountries to hide specific countries.

import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Your phone number"
      defaultCountry="us"
      onlyCountries={['us', 'gb', 'ca', 'au', 'de', 'fr']}
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}
import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Your phone number"
      defaultCountry="de"
      excludeCountries={['us', 'gb']}
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}

Custom flags

Use renderFlag prop to render custom flag elements, for example emoji flags. The function receives country data and should return a React node.

import { PhoneInput } from '@mantine/core';

function getEmojiFlag(iso2: string) {
  return iso2
    .toUpperCase()
    .split('')
    .map((char) => String.fromCodePoint(0x1f1e6 + char.charCodeAt(0) - 65))
    .join('');
}

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Your phone number"
      defaultCountry="us"
      renderFlag={(country) => (
        <span style={{ fontSize: 20, lineHeight: 1 }}>
          {country ? getEmojiFlag(country.iso2) : '🌐'}
        </span>
      )}
    />
  );
}

Searchable

Country dropdown search is enabled by default. Set searchable={false} to disable it.

import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Your phone number"
      defaultCountry="us"
      searchable={false}
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}

Country auto-detection

Set withCountryAutoDetect to automatically detect and switch the country when the user types a dial code. For example, typing +44 will switch to United Kingdom. Without the prop, the country still changes when a number starting with + and another dial code is typed or pasted. Area codes, for example +1 204 for Canada, are detected only with the prop:

import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Type +44 to switch to UK"
      defaultCountry="us"
      withCountryAutoDetect
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}

Localization

Use renderCountryOption to customize how countries are displayed in the dropdown. This can be used to translate country names to a different language.

getCountryLabel returns the translated name of a country. It is used as the country button label for screen readers and in the dropdown search, so users can search by translated names. searchPlaceholder and selectCountryLabel translate the search input placeholder and the button label shown before a country is selected:

import { PhoneInput } from '@mantine/core';

const COUNTRIES_ES: Record<string, string> = {
  us: 'Estados Unidos',
  gb: 'Reino Unido',
  es: 'España',
  mx: 'México',
  ar: 'Argentina',
};

function Demo() {
  return (
    <PhoneInput
      label="Número de teléfono"
      placeholder="Tu número de teléfono"
      defaultCountry="es"
      onlyCountries={['us', 'gb', 'es', 'mx', 'ar']}
      nothingFoundMessage="No se encontraron países"
      searchPlaceholder="Buscar países"
      selectCountryLabel="Seleccionar país"
      getCountryLabel={(country) => COUNTRIES_ES[country.iso2] ?? country.name}
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country ? COUNTRIES_ES[country.iso2] ?? country.name : 'Seleccionar país'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
      renderCountryOption={(country) => (
        <span style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%' }}>
          <img
            src={`https://hatscripts.github.io/circle-flags/flags/${country.iso2}.svg`}
            alt={COUNTRIES_ES[country.iso2] ?? country.name}
            style={{ width: 20, height: 20, borderRadius: 20 }}
          />
          <span style={{ flex: 1 }}>{COUNTRIES_ES[country.iso2] ?? country.name}</span>
          <span style={{ opacity: 0.5, fontSize: 'var(--mantine-font-size-xs)' }}>
            +{country.dialCode}
          </span>
        </span>
      )}
    />
  );
}

Dropdown position

import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Your phone number"
      defaultCountry="us"
      comboboxProps={{ position: 'top', middlewares: { flip: false, shift: false } }}
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}

Use within Popover

To use PhoneInput within Popover or other overlay, set comboboxProps={{ withinPortal: false }}. Pressing Escape while the country list is open closes both the list and the Popover. Set closeOnEscape={false} on the Popover to keep it open. The country list still closes on Escape:

import { Button, PhoneInput, Popover } from '@mantine/core';

function Demo() {
  return (
    <Popover width={300} position="bottom" withArrow shadow="md">
      <Popover.Target>
        <Button>Toggle popover</Button>
      </Popover.Target>
      <Popover.Dropdown>
        <PhoneInput
          label="Phone number"
          placeholder="Your phone number"
          defaultCountry="us"
          comboboxProps={{ withinPortal: false }}
          renderFlag={(country) => (
            <img
              src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
              alt={country?.name ?? 'Select country'}
              style={{ width: 20, height: 20, borderRadius: 20 }}
            />
          )}
        />
      </Popover.Dropdown>
    </Popover>
  );
}

Disabled state

import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Your phone number"
      defaultCountry="us"
      defaultValue="+12025551234"
      disabled
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}

Read only

import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Your phone number"
      defaultCountry="us"
      defaultValue="+12025551234"
      readOnly
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}

Error state

Invalid phone number

import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <>
      <PhoneInput
        label="Boolean error"
        placeholder="Your phone number"
        defaultCountry="us"
        error
        renderFlag={(country) => (
          <img
            src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
            alt={country?.name ?? 'Select country'}
            style={{ width: 20, height: 20, borderRadius: 20 }}
          />
        )}
      />
      <PhoneInput
        mt="md"
        label="With error message"
        placeholder="Your phone number"
        defaultCountry="us"
        error="Invalid phone number"
        renderFlag={(country) => (
          <img
            src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
            alt={country?.name ?? 'Select country'}
            style={{ width: 20, height: 20, borderRadius: 20 }}
          />
        )}
      />
    </>
  );
}

Success state

Looks good!

import { PhoneInput } from '@mantine/core';

function Demo() {
  return (
    <PhoneInput
      label="Phone number"
      placeholder="Your phone number"
      defaultCountry="us"
      success="Looks good!"
      renderFlag={(country) => (
        <img
          src={`https://hatscripts.github.io/circle-flags/flags/${country?.iso2 ?? 'xx'}.svg`}
          alt={country?.name ?? 'Select country'}
          style={{ width: 20, height: 20, borderRadius: 20 }}
        />
      )}
    />
  );
}

Styles API

PhoneInput supports the Styles API; you can add styles to any inner element of the component with the classNames prop. Follow the Styles API documentation to learn more.

Component Styles API

Hover over selectors to highlight corresponding elements

/*
 * Hover over selectors to apply outline styles
 *
 */

Get element ref

import { useRef } from 'react';
import { PhoneInput } from '@mantine/core';

function Demo() {
  const ref = useRef<HTMLInputElement>(null);
  return <PhoneInput ref={ref} />;
}

Accessibility

If PhoneInput is used without the label prop, it will not be announced properly by screen readers:

import { PhoneInput } from '@mantine/core';

// Inaccessible input – screen reader will not announce it properly
function Demo() {
  return <PhoneInput />;
}

Set aria-label to make the input accessible. In this case the label will not be visible, but screen readers will announce it:

import { PhoneInput } from '@mantine/core';

// Accessible input – it has aria-label
function Demo() {
  return <PhoneInput aria-label="My input" />;
}

If the label prop is set, the input will be accessible and it is not required to set aria-label:

import { PhoneInput } from '@mantine/core';

// Accessible input – it has associated label element
function Demo() {
  return <PhoneInput label="My input" />;
}