Phone number
A dialling country and a national number in one field. The country and the number are separate values, because storing them joined is a decision you should make, not one the field makes for you.
<PhoneInput
size="md"
label="Phone number"
required
optional
info="We only use this to secure your account."
defaultCountry="US"
hint="This is a hint text to help users."
/>Two values
onChange gives the number exactly as typed. onCountryChange gives the ISO code. Neither is formatted, normalised or validated. Yöte does not own validation, and phone numbers are the worst possible place to start.
const [country, setCountry] = useState('GB')
const [number, setNumber] = useState('')
<PhoneInput
label="Phone number"
country={country}
onCountryChange={setCountry}
value={number}
onChange={setNumber}
/>Countries
The default list is eight common entries, enough to see the component work and not a data set. Pass your own for anything real.
<PhoneInput
countries={[
{ code: 'FI', dial: '+358', name: 'Finland' },
{ code: 'SE', dial: '+46', name: 'Sweden' },
]}
defaultCountry="FI"
/>Flags
The eight defaults draw a real SVG flag, inline, with no request and no asset pipeline. The first version used the regional-indicator emoji, 🇬🇧 built from the letters G and B. It is free and needs nothing, and it is missing entirely on Windows: there it renders the two letters. A default that looks right on a Mac and broken on a PC is not a default.
A country the library does not draw still falls back to the emoji, so pass flag for anything outside the eight. It takes any node. The full 260 belong in your bundle rather than ours. The usual answer is flag-icons, which is where these eight came from, MIT, and pure CSS.
import 'flag-icons/css/flag-icons.min.css'
<PhoneInput
countries={[
{ code: 'FI', dial: '+358', name: 'Finland' },
{ code: 'EE', dial: '+372', name: 'Estonia', flag: <span className="fi fi-ee" /> },
]}
/>The picker
The list is a popover rather than a native <select>, because the design is a search field, a flag and a two-column row and a select can render none of those. Everything the select gave away for nothing is rebuilt explicitly: combobox roles, arrows with wrap, Home and End, Enter and Escape, and the highlighted option tracked with aria-activedescendant so focus stays in the search field and typing never breaks.
It renders into document.body and positions itself against the viewport, flipping above the field when there is no room below. That is not fussiness: a dropdown that stays inside the field is at the mercy of every ancestor, and one overflow: hidden on a card, a modal or a preview stage slices it in half. You do not control that and should not have to think about it.
Sizes
The left padding grows with the size while the right stays at 8px: the country group sits at the start and the number runs to the end.
| size | Radius | Padding left / right | Notes |
|---|---|---|---|
sm | 8px | 12px / 8px | Figma 26:9409. |
md | 12px | 14px / 8px | Figma 26:9410. The default. |
lg | 12px | 16px / 8px | Figma 26:9411. |
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
countries | PhoneCountry[] | eight common ones | The selectable dialling countries. |
country | string | — | Selected ISO code, controlled. |
defaultCountry | string | "US" | Selected ISO code, uncontrolled. |
onCountryChange | (code: string) => void | — | Fires when the country changes. |
required | boolean | false | Renders the asterisk and sets aria-required. |
optional | boolean | false | Renders the muted "(Optional)" note. |
info | string | — | Info marker beside the label, with this as its tooltip. |
Shared
| Prop | Type | Default | Notes |
|---|---|---|---|
value | string | — | Controlled value. |
defaultValue | string | — | Initial value when uncontrolled. |
onChange | (value: string) => void | — | Receives the value, never the event. |
label | ReactNode | — | Rendered as a real <label> wired by htmlFor. |
hint | ReactNode | — | Helper text under the field. |
error | ReactNode | — | Error text. Implies invalid unless invalid says otherwise. |
invalid | boolean | — | Forces the error styling on or off. |
errorKey | string | number | — | Change it to replay the error animation. |
disabled | boolean | false | Greys the field out. |
readOnly | boolean | false | Reads as filled, stays focusable. |
size | 'sm' | 'md' | 'lg' | 'md' | Size scale, where the component defines one. |
classNames | Record<Part, string> | — | Per-part class names. |