react-intl-phone-number
v0.2.0
Published
Framework-agnostic, antd-free international phone number input for React. E.164 in/out, searchable flag/country-code dropdown, configurable validation levels (strict / mobile-strict / loose), modern themeable CSS, i18n via a messages object or your own t(
Downloads
272
Maintainers
Readme
react-intl-phone-number
A framework-agnostic, Ant-Design-free international phone number input for React.
- 🌍 Searchable country dropdown — pick by flag or by calling code (
+60,+66, …). - 📞 E.164 in / out, powered by
google-libphonenumber. - 🎚️ Configurable, required
validationLevel:strict/mobile-strict/loose. - 🎨 Modern, themeable CSS — every part is a CSS variable and every node accepts a
classNamesoverride, so external utilities (e.g. Tailwind) win without!important. - 🌐 i18n via a
messagesobject (English defaults) and/or your ownt()function. - 🧩 Framework-agnostic core (
react-intl-phone-number/core) — validate without React.
Preview
| Leveled validation | Country / code picker | Format hints |
| :---: | :---: | :---: |
|
|
|
|
Install
npm i react-intl-phone-numberPeer dependencies (declared, so most package managers install them automatically):
npm i react react-dom google-libphonenumber
google-libphonenumberships full metadata (~hundreds of KB). It is a peer dependency so it is installed once and shared with the rest of your app rather than bundled into this package.
Quick start
import { useState } from "react"
import PhoneNumberInput from "react-intl-phone-number"
import "react-intl-phone-number/styles.css" // import once, anywhere in your app
function Example() {
const [phone, setPhone] = useState("") // E.164, e.g. "+66948383493"
return (
<PhoneNumberInput
value={phone}
onChange={setPhone}
defaultCountry="MY"
validationLevel="mobile-strict"
/>
)
}Using CommonJS / require? The component is the default (or the named export):
const { PhoneNumberInput, validatePhoneNumber } = require("react-intl-phone-number")Validation levels
validationLevel is required — you decide how strict per use case.
| level | mobile | landline |
| --- | --- | --- |
| strict | pattern-valid (isValidNumber) | pattern-valid |
| mobile-strict | pattern-valid | length-checked only |
| loose | length-checked only | length-checked only |
Note on
mobile-strict: in countries where mobile and landline share length ranges (US / MY / TH …), a possible-length number is accepted via the landline "length-only" branch, somobile-strictonly rejects a number that is mobile-length-possible, mobile-pattern-invalid, and not a possible landline length. The mobile-vs-landline distinction is only observable in regions whose mobile and landline lengths are disjoint.
Built-in error display
By default the component renders no error (you wire validation yourself). Opt in with
showError to render the validation message under the input after blur, or pass an
explicit error node (e.g. your "required" message):
// auto: shows the reason message (per validationLevel) once the field is blurred
<PhoneNumberInput validationLevel="strict" showError />
// explicit: you control the message (overrides the built-in one)
<PhoneNumberInput validationLevel="strict" error={isRequired ? "Phone is required" : undefined} />The control gets a red border and the input gets aria-invalid; the error text is
themeable via --ripn-error-color and classNames.error. Error wording comes from the
core single source (phoneReasonMessage) and bridges to your t when provided.
i18n
Both are optional; built-in English is used otherwise.
// (a) messages object — override any subset of strings
<PhoneNumberInput
validationLevel="strict"
messages={{ zeroHint: "Cannot start with 0", ruleHintTitle: "Accepted formats:" }}
/>
// (b) translate function (e.g. react-i18next) — wins over messages when both set.
// Keyed by Label_PhoneNumber_* so existing locale files work unchanged.
<PhoneNumberInput validationLevel="strict" t={(key, vars) => i18n.t(key, vars)} />
messagescovers the component-visible strings (placeholder, zero hint, format hints, number-type labels). Validation error wording lives in the core single source (phoneReasonMessage/validatePhoneNumber().message) and is shown viashowError/error; passtto translate it through your existingLabel_PhoneNumber_*keys.
Props
| prop | type | notes |
| --- | --- | --- |
| value | string | controlled E.164 |
| onChange | (value: string) => void | emits E.164 when valid, else a truthy +<cc><digits> partial |
| onBlur | () => void | |
| defaultCountry | CountryCode | changing it adopts the country and clears the number |
| disabled | boolean | |
| validationLevel | 'strict' \| 'mobile-strict' \| 'loose' | required |
| hintTypes | PhoneNumberKind[] | info-icon tooltip types; default mobile + landline |
| messages | Partial<Messages> | i18n overrides |
| t | (key, vars?) => string | translate fn; wins over messages |
| onValidityChange | (isValid: boolean) => void | computed only when provided |
| showError | boolean | render the built-in validation error under the input after blur |
| error | ReactNode | explicit error to show (overrides the built-in one; e.g. "required") |
| className / style | | on the root |
| classNames | Partial<Record<'root'\|'group'\|'select'\|'dropdown'\|'option'\|'input'\|'tooltip'\|'infoIcon'\|'error', string>> | per-part overrides (drop in Tailwind classes) |
| id / name / aria-label / inputRef | | form / a11y plumbing |
Theming
Override the CSS variables on .ripn-root (or globally), or pass classNames:
.ripn-root {
--ripn-radius: 12px;
--ripn-border-color-focus: #16a34a;
--ripn-error-color: #dc2626;
}Key tokens: --ripn-border-color, --ripn-border-color-hover,
--ripn-border-color-focus, --ripn-ring, --ripn-radius, --ripn-height,
--ripn-error-color, --ripn-warning-bg, --ripn-option-selected-bg,
--ripn-tooltip-bg. Prefer skipping styles.css entirely? Pass your own
classNames.* for full control.
Framework-agnostic core
import {
validatePhoneNumber,
phoneReasonMessage,
getPhoneNumberError,
phoneReasonI18nKey,
toE164,
} from "react-intl-phone-number/core"
// Every result carries the google-libphonenumber-derived `reason` CODE *and* a
// default English `message` — display it directly, or map the code yourself.
validatePhoneNumber("+66948383493", { level: "strict" })
// → { valid: true, reason: null, message: null }
validatePhoneNumber("+6612", { level: "strict" })
// → { valid: false, reason: "TOO_SHORT", message: "Phone number is too short." }
getPhoneNumberError("+6612") // "TOO_SHORT" (just the code; level defaults to "strict")
phoneReasonMessage("TOO_LONG") // "Phone number is too long."
phoneReasonI18nKey("TOO_SHORT") // "Label_PhoneNumber_TooShort" (for your own i18n)reason is one of INVALID_COUNTRY_CODE / NOT_A_NUMBER / TOO_SHORT / TOO_LONG /
INVALID_LENGTH / NOT_VALID (mirrors google-libphonenumber's classification).
No React, no DOM — usable in form validators or on a server.
License
MIT
