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.jsonpnpm dlx shadcn@latest add https://oui.open.gov.sg/r/phone-number-field.jsonnpx shadcn@latest add https://oui.open.gov.sg/r/phone-number-field.jsonbunx --bun shadcn@latest add https://oui.open.gov.sg/r/phone-number-field.jsonThe 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 customplaceholderprop 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 aPhoneNumberobject with properties likecountry,nationalNumber, andnumber.isPossiblePhoneNumber(value): Returnstrueif 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.errorMessageaccepts aReactNodeor 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 newE164Number | undefined.onCountryChange— Called when the selected country changes, receiving the newCountrycode.
Accessibility
- Built with a native
<input type="tel">element withautocomplete="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 wrappergroup: The field group containing the country selector and inputwrapper: The wrapper around the phone input elementslabel: The label text elementinput: The phone number input elementdescription: The description text below the fielderror: The error message textselect: The country selector wrapperselectTrigger: The country selector buttonselectIcon: The dropdown icon in the country selectorselectItem: Each item in the country dropdown listselectItemLabel: The country name label in each dropdown itemselectItemCountryCode: The calling code text in each dropdown item (e.g. "+65")flag: The country flag elementselectList: The country dropdown list containerselectPopover: 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 onvariantprop. - data-invalid: When the input is invalid. Based on
isInvalidprop. - data-disabled: When the input is disabled. Based on
isDisabledprop.
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
| Prop | Type | Default | Description |
|---|---|---|---|
label | React.ReactNode | - | The label for the phone number field |
description | React.ReactNode | - | The description text shown below the field |
errorMessage | React.ReactNode | ((validation: ValidationResult) => string) | - | The error message to display when validation fails |
value | E164Number | - | The current value in E.164 format (controlled) |
defaultValue | E164Number | - | The default value in E.164 format (uncontrolled) |
onChange | (value: E164Number | undefined) => void | - | Callback fired when the value changes |
isDisabled | boolean | false | Whether the field is disabled |
isInvalid | boolean | false | Whether the field should display as invalid |
defaultCountry | Country | "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 |
placeholder | string | - | Custom placeholder text for the input |
examples | Record<Country, string> | Mobile examples | Custom example phone numbers per country for placeholders |
international | boolean | false | Whether to display numbers in international format |
addInternationalOption | boolean | false | Whether to add an "International" option to the country selector |
countryOptionsOrder | Country[] | ["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 |
classNames | SlotsToClasses<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. |