@intl-ui/react
v1.0.5
Published
React components and hooks for international UI: phone input, country selector, city selector. Headless-first, accessible, framework-free styling.
Maintainers
Readme
@intl-ui/react
React components and hooks for international phone input. Headless-first, accessible, zero-config styling.
Part of the @intl-ui ecosystem.
Install
npm install @intl-ui/react
# or
pnpm add @intl-ui/react
# or
yarn add @intl-ui/react@intl-ui/core is bundled automatically — no separate install needed.
Peer dependency: react ^18.2.0 || ^19.0.0
Quick start
import { PhoneInput } from '@intl-ui/react';
function App() {
return (
<PhoneInput
defaultCountry="co"
onValueChange={(value, meta) => {
console.log(value); // "+573105551234"
console.log(meta.isValid); // true
console.log(meta.country); // { name: "Colombia", iso2: "co", ... }
}}
/>
);
}That's it. You get a fully working phone input with country dropdown, auto-detection, keyboard navigation, and validation. See it running in the live demo.
Three levels of abstraction
Pick the level that fits your use case. All three share the same core logic — you can always drop down a level for more control.
1. <PhoneInput /> — one-liner
Works out of the box with sensible defaults.
import { PhoneInput } from '@intl-ui/react';
<PhoneInput defaultCountry="us" onValueChange={(v) => console.log(v)} />2. Compound components — full layout control
Every child reads from a shared context. No prop drilling.
import { PhoneInput } from '@intl-ui/react';
<PhoneInput.Root defaultCountry="co" onValueChange={(v) => console.log(v)}>
<PhoneInput.CountrySelect>
<PhoneInput.Flag />
<PhoneInput.DialCode />
</PhoneInput.CountrySelect>
<PhoneInput.Input placeholder="Phone number" />
<PhoneInput.CountryList>
{(country, index) => (
<PhoneInput.CountryListItem
key={country.iso2}
country={country}
index={index}
/>
)}
</PhoneInput.CountryList>
</PhoneInput.Root>Every compound child supports className, style, event handlers, and the asChild prop for rendering your own element:
<PhoneInput.Input asChild>
<MyDesignSystemInput placeholder="Phone" />
</PhoneInput.Input>3. usePhoneInput() — headless hook
Maximum control. The hook returns state, prop-getters, and actions — you render whatever you want.
import { usePhoneInput } from '@intl-ui/react';
function CustomPhoneInput() {
const {
country, isValid, visibleCountries,
getInputProps, getCountrySelectProps,
getCountryListProps, getCountryOptionProps,
} = usePhoneInput({ defaultCountry: 'co' });
return (
<div>
<button {...getCountrySelectProps()}>
{country?.flag} +{country?.dialCode}
</button>
<input {...getInputProps()} />
</div>
);
}Features
- 200 countries with dial codes, format masks, capitals, flags, and regions
- Auto country detection — type
+380and Ukraine is detected via the dial-code trie - ISO code shortcut — type
co,usa,gbto switch country instantly - Auto-prefix
+— click the input and the+appears, ready for dial code digits - Backspace-aware — clear the input and type a new prefix without fighting the formatter
- Searchable dropdown — filter by name, ISO code, or dial code
- Keyboard navigation — arrows, Enter, Escape, typeahead
asChildpattern — clone your own elements with Radix-style Slot- Controlled + uncontrolled —
value/defaultValue+onValueChange, same as native<input> - Zero styling opinions — the package ships no CSS, use whatever you want
- 4.8 KB gzipped (hook + 7 compound components + Slot + Context)
Three ways to set the country
| Method | Example |
|---|---|
| Dropdown search | Click trigger → type colomb → pick Colombia |
| Type +code | Type +380 → Ukraine detected automatically |
| ISO shortcut | Type co, usa, gb, deu → instant switch |
Compound components
| Component | Element | Description |
|---|---|---|
| <PhoneInput /> | <div> | One-liner wrapper with sane defaults |
| <PhoneInput.Root> | — | Provider. Calls the hook, publishes via context |
| <PhoneInput.Input> | <input> | The phone number field (type="tel") |
| <PhoneInput.CountrySelect> | <button> | Trigger that opens the dropdown |
| <PhoneInput.Flag> | <span> | Emoji flag of the selected country |
| <PhoneInput.DialCode> | <span> | +57 label |
| <PhoneInput.CountryList> | <ul> | Dropdown listbox (render-prop children) |
| <PhoneInput.CountryListItem> | <li> | One country option |
Hook API — usePhoneInput(options)
Options
| Option | Type | Default | Description |
|---|---|---|---|
| value | string | — | Controlled phone value (E.164) |
| defaultValue | string | "" | Initial value for uncontrolled mode |
| onValueChange | (value, meta) => void | — | Fired on every change with parsed metadata |
| country | CountryIso2 | — | Controlled country ISO2 |
| defaultCountry | CountryIso2 | — | Initial country for uncontrolled mode |
| onCountryChange | (country) => void | — | Fired when the country changes |
| disableCountryGuess | boolean | false | Don't auto-detect country from typed input |
| countries | Country[] | all 200 | Custom country list |
| preferredCountries | CountryIso2[] | [] | Countries pinned to top of the dropdown |
Return value
| Field | Type | Description |
|---|---|---|
| value | string | Canonical E.164 string |
| inputValue | string | Formatted string displayed in the input |
| country | Country \| null | Selected country object |
| parsed | ParsedPhone \| null | Full parsed phone (e164, national, international, isValid, ...) |
| isValid | boolean | Whether the number is valid |
| isOpen | boolean | Dropdown open state |
| focusedIndex | number | Focused country index (-1 if none) |
| filter | string | Current dropdown filter text |
| visibleCountries | Country[] | Filtered country list |
| getInputProps | () => Props | Spread onto <input> |
| getCountrySelectProps | () => Props | Spread onto trigger <button> |
| getCountryListProps | () => Props | Spread onto <ul> |
| getCountryOptionProps | (country, index) => Props | Spread onto each <li> |
| setCountry | (iso2) => void | Set country programmatically |
| setOpen | (open) => void | Open/close dropdown |
| setFilter | (text) => void | Filter the country list |
| reset | () => void | Reset to initial state |
Advanced: custom compound children
Use usePhoneInputContext() to build your own compound children that read from <PhoneInput.Root>:
import { PhoneInput, usePhoneInputContext } from '@intl-ui/react';
function ValidationBadge() {
const { isValid, country } = usePhoneInputContext();
return isValid
? <span>✓ Valid {country?.name} number</span>
: <span>Enter a valid number</span>;
}
<PhoneInput.Root defaultCountry="co">
<PhoneInput.Input />
<ValidationBadge />
</PhoneInput.Root>Bundle size
| Package | Gzipped |
|---|---|
| @intl-ui/react | 4.8 KB |
| @intl-ui/core (bundled) | 9.4 KB |
| Total | ~14 KB |
Ecosystem
@intl-ui/core— Framework-agnostic phone parsing, formatting, validation, and country data. Works in Node, Deno, Bun, and the browser without React.@intl-ui/angular— Angular 17+ components, directives, and validators with signal-based reactivity and Reactive Forms support.
