PhoneInput
International phone field: country selector + formatted number input with field chrome.
Status: stable. Group: form. Markdown version: /design-system/components/phone-input.md
When to use
PhoneInput wraps react-phone-number-input with the design-system field chrome (FieldShell: label, status message, helper text and label-row actions) and token-based styling over the library's country selector and input. It formats as you type and emits the E.164 value (or undefined while incomplete) through onChange. The default country is FI; override defaultCountry per audience.
When not to use
- Do not bypass onChange and read the DOM input value; it is the display format, not the E.164 value.
- Do not use TextInput type=tel for international audiences; you lose country selection and normalization.
Look-alikes
- TextInput: Do not use TextInput type=tel for international audiences; you lose country selection and normalization.
Props
| Prop | Type | Default | Description | Required |
|---|---|---|---|---|
label | string | none | Label text. | Yes |
value | string | none | Phone number value (E.164 format) | No |
error | string | none | Error message; renders above the helper line. | No |
status | FieldStatus | none | Semantic state: colours the border and styles statusMessage. error overrides it. | No |
statusMessage | string | none | Message under the field for status, above the helper line. | No |
actions | FieldAction[] | none | Icon buttons at the end of the label row: each runs an operation or opens a menu. | No |
helperText | string | none | Helper text below the input. | No |
placeholder | string | none | Placeholder text. | No |
disabled | boolean | false | Disabled state. | No |
required | boolean | false | Shows the required marker on the label. | No |
id | string | none | Explicit control id (wires the label); auto-generated when omitted. | No |
defaultCountry | "AC" | "AD" | "AE" | "AF" | "AG" | "AI" | "AL" | "AM" | "AO" | "AR" | "AS" | "AT" | "AU" | "AW" | "AX" | "AZ" | "BA" | "BB" | "BD" | "BE" | "BF" | "BG" | "BH" | "BI" | "BJ" | "BL" | "BM" | "BN" | "BO" | "BQ" | "BR" | "BS" | "BT" | "BW" | "BY" | "BZ" | "CA" | "CC" | "CD" | "CF" | "CG" | "CH" | "CI" | "CK" | "CL" | "CM" | "CN" | "CO" | "CR" | "CU" | "CV" | "CW" | "CX" | "CY" | "CZ" | "DE" | "DJ" | "DK" | "DM" | "DO" | "DZ" | "EC" | "EE" | "EG" | "EH" | "ER" | "ES" | "ET" | "FI" | "FJ" | "FK" | "FM" | "FO" | "FR" | "GA" | "GB" | "GD" | "GE" | "GF" | "GG" | "GH" | "GI" | "GL" | "GM" | "GN" | "GP" | "GQ" | "GR" | "GT" | "GU" | "GW" | "GY" | "HK" | "HN" | "HR" | "HT" | "HU" | "ID" | "IE" | "IL" | "IM" | "IN" | "IO" | "IQ" | "IR" | "IS" | "IT" | "JE" | "JM" | "JO" | "JP" | "KE" | "KG" | "KH" | "KI" | "KM" | "KN" | "KP" | "KR" | "KW" | "KY" | "KZ" | "LA" | "LB" | "LC" | "LI" | "LK" | "LR" | "LS" | "LT" | "LU" | "LV" | "LY" | "MA" | "MC" | "MD" | "ME" | "MF" | "MG" | "MH" | "MK" | "ML" | "MM" | "MN" | "MO" | "MP" | "MQ" | "MR" | "MS" | "MT" | "MU" | "MV" | "MW" | "MX" | "MY" | "MZ" | "NA" | "NC" | "NE" | "NF" | "NG" | "NI" | "NL" | "NO" | "NP" | "NR" | "NU" | "NZ" | "OM" | "PA" | "PE" | "PF" | "PG" | "PH" | "PK" | "PL" | "PM" | "PR" | "PS" | "PT" | "PW" | "PY" | "QA" | "RE" | "RO" | "RS" | "RU" | "RW" | "SA" | "SB" | "SC" | "SD" | "SE" | "SG" | "SH" | "SI" | "SJ" | "SK" | "SL" | "SM" | "SN" | "SO" | "SR" | "SS" | "ST" | "SV" | "SX" | "SY" | "SZ" | "TA" | "TC" | "TD" | "TG" | "TH" | "TJ" | "TK" | "TL" | "TM" | "TN" | "TO" | "TR" | "TT" | "TV" | "TW" | "TZ" | "UA" | "UG" | "US" | "UY" | "UZ" | "VA" | "VC" | "VE" | "VG" | "VI" | "VN" | "VU" | "WF" | "WS" | "XK" | "YE" | "YT" | "ZA" | "ZM" | "ZW" | FI | ISO 3166-1 alpha-2 code used when the value has no country prefix. | No |
size | "sm" | "md" | "lg" | md | Field size, matching TextInput; lg is the 3rem height every lg field shares. | No |
onChange | (value: string | undefined) => void | none | No description in the contract. | No |
Variants
- Available sizes:
sm,md,lg(defaultmd). Forwarded to CSS module size classes. - Allowed
defaultCountryvalues:AC,AD,AE,AF,AG,AI,AL,AM,AO,AR,AS,AT,AU,AW,AX,AZ,BA,BB,BD,BE,BF,BG,BH,BI,BJ,BL,BM,BN,BO,BQ,BR,BS,BT,BW,BY,BZ,CA,CC,CD,CF,CG,CH,CI,CK,CL,CM,CN,CO,CR,CU,CV,CW,CX,CY,CZ,DE,DJ,DK,DM,DO,DZ,EC,EE,EG,EH,ER,ES,ET,FI,FJ,FK,FM,FO,FR,GA,GB,GD,GE,GF,GG,GH,GI,GL,GM,GN,GP,GQ,GR,GT,GU,GW,GY,HK,HN,HR,HT,HU,ID,IE,IL,IM,IN,IO,IQ,IR,IS,IT,JE,JM,JO,JP,KE,KG,KH,KI,KM,KN,KP,KR,KW,KY,KZ,LA,LB,LC,LI,LK,LR,LS,LT,LU,LV,LY,MA,MC,MD,ME,MF,MG,MH,MK,ML,MM,MN,MO,MP,MQ,MR,MS,MT,MU,MV,MW,MX,MY,MZ,NA,NC,NE,NF,NG,NI,NL,NO,NP,NR,NU,NZ,OM,PA,PE,PF,PG,PH,PK,PL,PM,PR,PS,PT,PW,PY,QA,RE,RO,RS,RU,RW,SA,SB,SC,SD,SE,SG,SH,SI,SJ,SK,SL,SM,SN,SO,SR,SS,ST,SV,SX,SY,SZ,TA,TC,TD,TG,TH,TJ,TK,TL,TM,TN,TO,TR,TT,TV,TW,TZ,UA,UG,US,UY,UZ,VA,VC,VE,VG,VI,VN,VU,WF,WS,XK,YE,YT,ZA,ZM,ZW(defaultFI).
Invalid combinations
The contract records no invalid combinations.
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Moves focus to the control and on to the next focusable element. (standard) |
| Arrow keys | (country select) |
| Home / End | (country select, while the list is open) |
| Enter, Space, Escape | And typeahead by name or calling code (country select) |
| General | Typing digits. |
Rows marked (standard) name a key the contract lists without a description; the behaviour shown is the usual one for that key.
Rules
Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.
| Level | Rule | How it is checked |
|---|---|---|
| MUST | ARIA: Label associated via htmlFor/id. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Error via HelperText linked with aria-describedby + aria-invalid. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Country picker is a select-only combobox (aria-expanded, aria-controls, aria-activedescendant) named from the library's localized label only; the visible "FI +358" text is not part of the accessible name. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by npm run test:stories:matrix:ci. WCAG 2.5.8. |
| MUST | The accessibility tree (role, name, state) matches the committed snapshot for every required story. | Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by npm run test:stories:hc. |
| MUST | Every documented keyboard interaction works as specified. | Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-11-04. |
| SHOULD | Validate with isValidPhoneNumber from react-phone-number-input on submit; the field formats but does not block invalid numbers. | Not stated in the contract. |
| SHOULD | Store and send the emitted E.164 value verbatim; it is the interchange format your backend and SMS providers expect. | Not stated in the contract. |
| SHOULD | Set defaultCountry to the audience you actually serve; users typing a local number get the right prefix for free. | Not stated in the contract. |
| SHOULD | Treat onChange emitting undefined as "incomplete", not as an error; only validate on blur or submit. | Not stated in the contract. |
| SHOULD NOT | Do not pre-split country code and number into two fields; the combined control is the accessible, error-proof pattern. | Not stated in the contract. |
Install and import
- Install:
npm install @digitaltableteur/react(version 0.9.1, restricted access on npm). - Import:
import { PhoneInput } from "@digitaltableteur/react"; - Entry point:
@digitaltableteur/react/formsexports it too. - Styles: import
@digitaltableteur/react/style.cssonce at the app boundary. - Also available as the custom element
<dt-phone-input>; see the custom elements manifest.
Links
- Machine-readable contract (Design System Contract 1.0)
- Markdown version of this page
- Component index as JSON
- All components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.