Skip to content

InputPhone

For API documentation, refer to the components used in the InputPhone pattern:

Unknown country and number format

The input is pre-filled with the value: +.

tsx
import { Flex } from '@semcore/ui/base-components';
import { ButtonLink } from '@semcore/ui/button';
import CloseM from '@semcore/ui/icon/Close/m';
import Input from '@semcore/ui/input';
import { Hint } from '@semcore/ui/tooltip';
import { Text } from '@semcore/ui/typography';
import React from 'react';

const Demo = () => {
  const [value, setValue] = React.useState('+');
  return (
    <Flex direction='column'>
      <Text tag='label' htmlFor='basic-example' size={200}>
        Phone number
      </Text>
      <Input w={180} mt={2}>
        <Input.Value
          id='basic-example'
          type='tel'
          autoComplete='tel'
          value={value}
          onChange={(v) => setValue(v)}
        />
        {value.length > 1 && (
          <Input.Addon>
            <ButtonLink
              use='secondary'
              addonLeft={CloseM}
              title='Clear'
              onClick={() => setValue('+')}
            />
          </Input.Addon>
        )}
      </Input>
    </Flex>
  );
};

export default Demo;

Known country and unknown number format

The input has a preset value: "{country code}". However, if it's possible to enter phone numbers from multiple countries, a country select option should be provided instead of a static flag.

TIP

Some countries may have multiple formats for phone numbers. Therefore, it's sometimes safer to remove the format mask to avoid restricting user input.

tsx
import { Flex } from '@semcore/ui/base-components';
import { ButtonLink } from '@semcore/ui/button';
import Flag, { iso2Name } from '@semcore/ui/flags';
import CloseM from '@semcore/ui/icon/Close/m';
import Input from '@semcore/ui/input';
import { Text } from '@semcore/ui/typography';
import React from 'react';

const Demo = () => {
  const [value, setValue] = React.useState('1');
  return (
    <Flex direction='column'>
      <Text tag='label' size={200} htmlFor='phone-number' id='phone-label'>
        Phone number
      </Text>
      <Input w={180} mt={2}>
        <Input.Addon>
          <Flag iso2='US' role='img' aria-label={iso2Name['US']} id='country-flag' />
        </Input.Addon>
        <Input.Value
          value={value}
          onChange={(v) => setValue(v)}
          id='phone-number'
          type='tel'
          autoComplete='tel'
          aria-labelledby='phone-label country-flag'
        />
        {Number.parseInt(value, 10) > 2 && (
          <Input.Addon>
            <ButtonLink
              use='secondary'
              addonLeft={CloseM}
              title='Clear'
              onClick={() => setValue('1')}
            />
          </Input.Addon>
        )}
      </Input>
    </Flex>
  );
};

export default Demo;

Known country and number format

WARNING

InputMask is deprecated and will be removed in the next major release.

The input includes the country code as a preset value: "+ {country code}". Additionally, it has the required format mask.

This option is suitable when collecting phone numbers from users in one or a limited number of countries.

TIP

Make sure to verify the available valid phone number formats for the specific country you are targeting.

tsx
import { Box, Flex } from '@semcore/ui/base-components';
import { ButtonLink } from '@semcore/ui/button';
import Flag from '@semcore/ui/flags';
import CloseM from '@semcore/ui/icon/Close/m';
import Input from '@semcore/ui/input';
import InputMask from '@semcore/ui/input-mask';
import Select from '@semcore/ui/select';
import { Text } from '@semcore/ui/typography';
import React from 'react';

const Demo = () => {
  const inputRef = React.useRef<HTMLInputElement>(null);
  const [country, setCountry] = React.useState<keyof typeof countries>('DE');
  const prefix = countries[country].prefix;
  const [phoneNumber, setPhoneNumber] = React.useState(prefix);
  const [phoneMask, setPhoneMask] = React.useState(`${prefix} (___)___-____`);
  const positionAfterFirstBracket = prefix.length + 2;

  const handleChange = (value: string, e: React.SyntheticEvent<HTMLInputElement>) => {
    setPhoneNumber(value);

    if (value === prefix) {
      inputRef.current?.setSelectionRange(positionAfterFirstBracket, positionAfterFirstBracket);
    }

    if (e.currentTarget.selectionStart === 0) {
      e.currentTarget.setSelectionRange(positionAfterFirstBracket, positionAfterFirstBracket);
    }
  };

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Backspace' || e.key === 'ArrowLeft') {
      if (e.currentTarget.value === prefix) {
        e.preventDefault();
        inputRef.current?.setSelectionRange(positionAfterFirstBracket, positionAfterFirstBracket);

        return false;
      }

      const selectionStart = inputRef.current?.selectionStart ?? 0;
      const selectionEnd = inputRef.current?.selectionEnd ?? 0;
      if (selectionStart <= positionAfterFirstBracket) {
        if (selectionStart === selectionEnd) {
          e.preventDefault();

          return false;
        } else {
          setTimeout(() => {
            inputRef.current?.setSelectionRange(
              positionAfterFirstBracket,
              positionAfterFirstBracket,
            );
          }, 0);

          return false;
        }
      }
    }

    if (e.key === 'ArrowUp' || (e.key === 'ArrowLeft' && e.metaKey) || e.key === 'Home') {
      e.preventDefault();
      inputRef.current?.setSelectionRange(positionAfterFirstBracket, positionAfterFirstBracket);
    }
  };

  const handleClick = (e: React.SyntheticEvent<HTMLInputElement>) => {
    if (
      e.currentTarget instanceof HTMLInputElement &&
      (e.currentTarget.selectionStart ?? 0) <= positionAfterFirstBracket
    ) {
      e.preventDefault();
      inputRef.current?.setSelectionRange(positionAfterFirstBracket, positionAfterFirstBracket);
    }
  };

  return (
    <Flex direction='column'>
      <Text tag='label' htmlFor='phone-number-with-country-select' size={200}>
        Phone number
      </Text>
      <Box mt={2}>
        <Select
          value={country}
          onChange={(newCountry: keyof typeof countries) => {
            setCountry(newCountry);
            const prefix = countries[newCountry].prefix;
            setPhoneNumber(prefix);
            setPhoneMask(`${prefix} (___)___-____`);
            setTimeout(() => {
              inputRef?.current?.focus();
            }, 1);
          }}
        >
          <Select.Trigger aria-label='Country code' neighborLocation='right'>
            <Select.Trigger.Addon mx={0}>
              <Flag role='img' iso2={country} aria-label={countries[country].name} />
            </Select.Trigger.Addon>
          </Select.Trigger>

          <Select.Menu>
            {Object.keys(countries).map((country) => {
              const countryKey = country as keyof typeof countries;
              return (
                <Select.Option key={countryKey} value={countryKey}>
                  <Text size={200} mr={2} aria-hidden='true'>
                    <Flag iso2={countryKey} />
                  </Text>
                  <Text size={200} mr={2}>
                    {countries[countryKey].name}
                  </Text>
                  <Text size={200} color='text-secondary'>
                    {countries[countryKey].prefix}
                  </Text>
                </Select.Option>
              );
            })}
          </Select.Menu>
        </Select>
        <InputMask w={210} neighborLocation='left'>
          <InputMask.Value
            id='phone-number-with-country-select'
            ref={inputRef}
            value={phoneNumber}
            onChange={handleChange}
            aliases={{ _: /\d/ }}
            mask={phoneMask}
            type='tel'
            autoComplete='tel'
            title='10 digits, without country code'
            onKeyDown={handleKeyDown}
            onClick={handleClick}
          />
          {phoneNumber !== prefix && (
            <Input.Addon>
              <ButtonLink
                use='secondary'
                addonLeft={CloseM}
                title='Clear'
                onClick={() => setPhoneNumber(prefix)}
              />
            </Input.Addon>
          )}
        </InputMask>
      </Box>
    </Flex>
  );
};

const countries = {
  DE: { name: 'Germany', prefix: '+49' },
  IT: { name: 'Italy', prefix: '+39' },
  NL: { name: 'Netherlands', prefix: '+31' },
  GB: { name: 'United Kingdom', prefix: '+44' },
};

export default Demo;

Released under the MIT License.

Released under the MIT License.