Open UI

PhoneNumberField

A phone number field allows users to enter and edit phone numbers using a keyboard.

import { PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  return <PhoneNumberField label="Contact number" />}

Usage

import { PhoneNumberField } from "@opengovsg/oui"
<PhoneNumberField label="Contact number" />

Alternatively, install the component as local source via the shadcn CLI:

npx shadcn@latest add https://oui.open.gov.sg/r/phone-number-field.json
pnpm dlx shadcn@latest add https://oui.open.gov.sg/r/phone-number-field.json
npx shadcn@latest add https://oui.open.gov.sg/r/phone-number-field.json
bunx --bun shadcn@latest add https://oui.open.gov.sg/r/phone-number-field.json

The PhoneNumberField component provides an accessible phone number input with a built-in country selector. It is built on top of react-phone-number-input and stores values in E.164 international format (e.g. +6591234567).

By default, the country selector is set to Singapore (SG) and the input displays an example phone number as a placeholder for the selected country.

If the component does not have a visible label (by passing a label prop), an aria-label or aria-labelledby prop must be passed instead to identify it to assistive technology.

Anatomy

PhoneNumberField consists of:

  • PhoneNumberField — the root field container; manages value, country, and validation state
    • Label — the visible label element
    • FieldGroup — the bordered container wrapping the country selector and input
      • CountrySelect — the flag + dropdown that selects the dialing country (international variant only)
      • PhoneInput — the <input type="tel"> element
    • Description — helper text below the field
    • FieldError — error message shown when invalid

Examples

With Label and Description

Provide clear instructions to users with labels and descriptions.

import { PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  return (    <PhoneNumberField      label="Contact number"      description="The contact number of the individual."    />  )}

Local Variant

Use the variant="local" prop to render a simplified phone number field without the country selector. This is useful when only local phone numbers are expected. The field displays the country flag based on the defaultCountry prop (defaults to "SG").

import { PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  return <PhoneNumberField variant="local" label="Contact number" />}

Placeholder Modes

The placeholderMode prop controls how the input placeholder behaves in relation to example phone numbers for the selected country.

  • "polite" (default): Uses the example number as placeholder only if no custom placeholder prop is provided.
  • "aggressive": Always replaces the placeholder with the example number for the selected country.
  • "off": Never uses example numbers as placeholders.
import { PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  return (    <div className="flex flex-col gap-6">      <PhoneNumberField label="Polite (default)" placeholderMode="polite" />      <PhoneNumberField label="Aggressive" placeholderMode="aggressive" />      <PhoneNumberField label="Off" placeholderMode="off" />    </div>  )}

With Custom Placeholder

Use the placeholder prop to show custom placeholder text. In "polite" mode (the default), the custom placeholder takes precedence over the example number.

import { PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  return (    <PhoneNumberField label="Contact number" placeholder="e.g. 9123 4567" />  )}

Default Country

Use the defaultCountry prop to set the initially selected country. Defaults to "SG" (Singapore). The default country will also be ordered at the top of the country selector dropdown, unless overridden by the countryOptionsOrder prop.

import { PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  return <PhoneNumberField label="Contact number" defaultCountry="MY" />}

Country Options Order

Use the countryOptionsOrder prop to control which countries appear at the top of the country selector dropdown.

import { PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  return (    <PhoneNumberField      label="Contact number"      countryOptionsOrder={["SG", "MY"]}    />  )}

Controlled

Use the value and onChange props to control the phone number value programmatically. The value is stored in E.164 format (e.g. "+6591234567").

import { useState } from "react"import type { E164Number } from "@opengovsg/oui"import { formatPhoneNumberIntl, PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  const [value, setValue] = useState<E164Number | undefined>()  return (    <div className="flex flex-col gap-4">      <PhoneNumberField        label="Contact number"        value={value}        onChange={setValue}      />      <p className="text-sm text-gray-600">        Value: {value ? formatPhoneNumberIntl(value) : "empty"}      </p>    </div>  )}

With Error Message

Combine isInvalid and errorMessage props to show validation errors.

import { PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  return (    <PhoneNumberField      label="Contact number"      isInvalid      errorMessage="Please enter a valid contact number."    />  )}

With Validation

Use the isPossiblePhoneNumber utility function (re-exported from react-phone-number-input) to validate phone numbers on change.

import { useState } from "react"import { isPossiblePhoneNumber, PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  const [errorMessage, setErrorMessage] = useState<string>()  return (    <PhoneNumberField      label="Contact number"      errorMessage={errorMessage}      isInvalid={!!errorMessage}      onChange={(value) => {        if (value && !isPossiblePhoneNumber(value)) {          setErrorMessage("Please enter a valid contact number.")        } else {          setErrorMessage(undefined)        }      }}    />  )}

Sizes

Use the size prop to change the size of the phone number field.

import { PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  return (    <div className="flex flex-col gap-6">      <PhoneNumberField size="xs" label="Extra Small" />      <PhoneNumberField size="sm" label="Small" />      <PhoneNumberField size="md" label="Medium (default)" />    </div>  )}

Disabled

Use the isDisabled prop to disable the phone number field.

import { PhoneNumberField } from "@opengovsg/oui"export const Example = () => {  return <PhoneNumberField label="Contact number" isDisabled />}

Utility Functions

PhoneNumberField re-exports the following utility functions from react-phone-number-input:

  • formatPhoneNumber(value): Formats an E.164 phone number to national format (e.g. "8123 4567").
  • formatPhoneNumberIntl(value): Formats an E.164 phone number to international format (e.g. "+65 8123 4567").
  • parsePhoneNumber(value): Parses an E.164 phone number string into a PhoneNumber object with properties like country, nationalNumber, and number.
  • isPossiblePhoneNumber(value): Returns true if the phone number has a valid length for its country. This is the recommended validation approach.
import {
  formatPhoneNumber,
  formatPhoneNumberIntl,
  isPossiblePhoneNumber,
  parsePhoneNumber,
} from "@opengovsg/oui"

Validation

See the Forms guide for end-to-end patterns including React Hook Form + Zod.

  • isInvalid + errorMessage — use together for realtime validation feedback. errorMessage accepts a ReactNode or a function (validation: ValidationResult) => string.
  • isPossiblePhoneNumber — the recommended way to validate phone number values; checks whether the number has a valid length for the detected country.

PhoneNumberField does not support native HTML constraint validation (isRequired, minLength, etc.) because the underlying react-phone-number-input library manages its own input — use controlled state and isInvalid instead.

Events

  • onChange — Called when the phone number value changes, receiving the new E164Number | undefined.
  • onCountryChange — Called when the selected country changes, receiving the new Country code.

Accessibility

  • Built with a native <input type="tel"> element with autocomplete="tel"
  • Country selector is keyboard accessible with built-in search
  • Full keyboard support for country selection and phone number entry
  • Proper ARIA labeling and descriptions
  • Invalid and disabled states exposed to assistive technology
  • Country flags include accessible country name labels

Slots

Slots are named regions of the component you can target with custom Tailwind classes via the classNames prop. Each slot below corresponds to a key on the classNames object.

  • base: The root container wrapper
  • group: The field group containing the country selector and input
  • wrapper: The wrapper around the phone input elements
  • label: The label text element
  • input: The phone number input element
  • description: The description text below the field
  • error: The error message text
  • select: The country selector wrapper
  • selectTrigger: The country selector button
  • selectIcon: The dropdown icon in the country selector
  • selectItem: Each item in the country dropdown list
  • selectItemLabel: The country name label in each dropdown item
  • selectItemCountryCode: The calling code text in each dropdown item (e.g. "+65")
  • flag: The country flag element
  • selectList: The country dropdown list container
  • selectPopover: The country dropdown popover container

Data Attributes

PhoneNumberField has the following attributes on the base element, which you can use to style the component based on its state (e.g. group-[data-invalid=true]:bg-red-500):

  • data-variant: The current variant of the field ("international" or "local"). Based on variant prop.
  • data-invalid: When the input is invalid. Based on isInvalid prop.
  • data-disabled: When the input is disabled. Based on isDisabled prop.

Custom Styles

You can customize the PhoneNumberField component by passing custom Tailwind CSS classes to the component slots via the classNames prop.

<PhoneNumberField
  label="Contact number"
  classNames={{
    base: "max-w-xs",
    input: "text-right",
    flag: "rounded-md",
  }}
/>

Props

PhoneNumberField

PropTypeDefaultDescription
labelReact.ReactNode-The label for the phone number field
descriptionReact.ReactNode-The description text shown below the field
errorMessageReact.ReactNode | ((validation: ValidationResult) => string)-The error message to display when validation fails
valueE164Number-The current value in E.164 format (controlled)
defaultValueE164Number-The default value in E.164 format (uncontrolled)
onChange(value: E164Number | undefined) => void-Callback fired when the value changes
isDisabledbooleanfalseWhether the field is disabled
isInvalidbooleanfalseWhether the field should display as invalid
defaultCountryCountry"SG"The initially selected country
onCountryChange(country: Country) => void-Callback fired when the selected country changes
placeholderMode"polite" | "aggressive" | "off""polite"Controls how example number placeholders are shown
placeholderstring-Custom placeholder text for the input
examplesRecord<Country, string>Mobile examplesCustom example phone numbers per country for placeholders
internationalbooleanfalseWhether to display numbers in international format
addInternationalOptionbooleanfalseWhether to add an "International" option to the country selector
countryOptionsOrderCountry[]["SG"]Countries to display at the top of the selector
variant"international" | "local""international"Whether to use the international phone input with country select or a local-only input with a fixed country flag
size"xs" | "sm" | "md""md"The size of the component
classNamesSlotsToClasses<PhoneNumberFieldSlots>-Custom CSS classes for component slots
onClear() => void-Called when the user clears the input (via the internal PhoneInput clear mechanism — Cmd+Backspace on macOS). Provided automatically by PhoneNumberField; only pass this if using PhoneInput directly.

On this page