# PhoneInput

> International phone field: country selector + formatted number input with field chrome.

- Status: stable
- Group: form
- Page: https://www.digitaltableteur.com/design-system/components/phone-input
- Contract: https://www.digitaltableteur.com/contracts/v1/PhoneInput.contract.json

## 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` (default `md`). Forwarded to CSS module size classes.
- Allowed `defaultCountry` values: `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` (default `FI`).

## 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/forms` exports it too.
- Styles: import `@digitaltableteur/react/style.css` once 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): https://www.digitaltableteur.com/contracts/v1/PhoneInput.contract.json
- Markdown version of this page: https://www.digitaltableteur.com/design-system/components/phone-input.md
- Component index as JSON: https://www.digitaltableteur.com/design-system/components.json
- All components: https://www.digitaltableteur.com/design-system/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.
