Phone input
A phone number as a country with its calling code and the national number, stored in E.164 form.
On this page
Code
import { PhoneInput } from "@bosunkit/components/phone-input";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A crew member's phone, required, starting in the vessel's country: the country button opens a list searched by " +
"name or calling code, the number groups as it is typed, a pasted international number brings its country, and " +
"the page receives E.164; beside it an emergency contact's French number.";
export default function Showcase(): ReactElement {
const [phone, setPhone]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
return (
<div style={{ width: "100%", maxWidth: 420, display: "grid", gap: "var(--space-lg)" }}>
<PhoneInput
label="Phone"
required
defaultCountry="GB"
value={phone}
onValueChange={setPhone}
hint="Paste +31 6 12345678 to see the country follow."
/>
<PhoneInput label="Emergency phone" defaultValue="+33639981234" />
<output style={{ fontSize: 12, color: "var(--text-dim)" }}>
The page receives: {phone === "" ? "no number yet" : phone}
</output>
</div>
);
}
A phone input is a country button and the national number. The button shows the country's code and calling code
(GB +44) and opens a list of every country, named in the crew's language and searched by name, code or calling
code. The field starts in defaultCountry, which the page sets to the vessel's country. The national digits are
grouped as they are typed the way the country writes them after its calling code (7345 615225 in the United
Kingdom, 20 7946 0958 in London, 6 39 98 12 34 in France), from the formats of Google's libphonenumber; a country
it has no format for shows the digits as typed. The grouping is for reading only, and the value is the digits.
The page receives the number in E.164 form (+447700900123). The national number loses the trunk prefix dialed only
inside the country, by libphonenumber's rules for it: 0 in the United Kingdom, 1 in North America, 8 in Russia, 06 in
Hungary. A country without one keeps its leading 0, which is part of the number (Italy, Benin), and a prefix that is
part of the number stays (a Russian 800 number typed without its 8).
A number typed or pasted in international form (+31 6 12345678, 0044 7700 900123) sets the country, as soon as
its calling code is complete. The field refuses what bosund's E.164 pattern refuses: more than 15 digits with the
calling code.
A whole number is checked against libphonenumber's number patterns, as its isValidNumber and getNumberType check
it. Under the field it names the number's kind and the country it belongs to (Mobile, United Kingdom, Landline, Guernsey for a +44 number in Guernsey's ranges), telling apart landlines, mobiles, toll-free, premium-rate,
shared-cost, VoIP, personal, pager, business and voicemail numbers where the country's plan does. A number no country
with its calling code has is doubted once the crew leave the field, by its cause: too few digits (or only enough to
dial locally), too many, a length no number has, or no number starting this way. The doubt never makes the field
invalid, and the field reports the number as typed: the patterns age, and a range opened after the build is a real
number.
The patterns load in a chunk of their own once the field holds a number.
The number input takes digits only. Text with a letter in it is kept as typed and not read: the field says letters and what follows them are not saved, so an extension typed after a number ("ext 1") never joins its digits.
Extension
A number that reaches a switchboard (a marina office, an agent, a yard) can have an extension. E.164 has none, so
showExtension adds an input for it after the number, marked with the crew's word for it (ext., Dw., poste, int.,
tst.), with autocomplete="tel-extension" so a browser fills it from the contact. It takes digits, is reported apart
from the number through onExtensionChange and sent with a form under extensionName. A letter typed after the
number moves the focus to it, so "07700 900123 ext 12" fills both. A crew member's mobile has no extension, and a form
that does not ask for one leaves showExtension off.
Code
import { PhoneInput } from "@bosunkit/components/phone-input";
import type { ReactElement } from "react";
export const description: string =
"A marina office's switchboard number with its extension, which goes in an input of its own after the number.";
export default function Extension(): ReactElement {
return <PhoneInput label="Marina office" defaultValue="+442079460123" showExtension defaultExtension="214" />;
}
Examples
Empty
Code
import { PhoneInput } from "@bosunkit/components/phone-input";
import type { ReactElement } from "react";
export const description: string =
"An empty number in the vessel's country, which the page passes as `defaultCountry`; a phone shows its dial pad.";
export default function Empty(): ReactElement {
return <PhoneInput label="Phone" defaultCountry="NL" />;
}
Filled
Code
import { PhoneInput } from "@bosunkit/components/phone-input";
import type { ReactElement } from "react";
export const description: string =
"A held E.164 number shows in its own country, its national digits grouped for reading.";
export default function Filled(): ReactElement {
return <PhoneInput label="Phone" defaultValue="+447700900123" />;
}
Numbers as written at home
| GB | 07700 900123 | 7700 900123 | +447700900123 |
| NL | 06-12345678 | 6 12345678 | +31612345678 |
| FR | 06 39 98 12 34 | 6 39 98 12 34 | +33639981234 |
| US | (202) 555-0123 | 202-555-0123 | +12025550123 |
| IT | 06 1234 5678 | 06 1234 5678 | +390612345678 |
Code
import { formatNational, nationalDigits, toE164 } from "@bosunkit/components/phone-input";
import type { ReactElement } from "react";
export const description: string =
"Numbers as the crew write them at home, and the E.164 form the field reports: the trunk 0 goes, except in Italy, " +
"where it is part of the number.";
interface Written {
readonly country: string;
readonly text: string;
}
const NUMBERS: readonly Written[] = [
{ country: "GB", text: "07700 900123" },
{ country: "NL", text: "06-12345678" },
{ country: "FR", text: "06 39 98 12 34" },
{ country: "US", text: "(202) 555-0123" },
{ country: "IT", text: "06 1234 5678" },
];
export default function NationalForms(): ReactElement {
return (
<table style={{ fontFamily: "var(--font-mono)", fontSize: 13, borderCollapse: "collapse" }}>
<tbody>
{NUMBERS.map((number: Written): ReactElement => {
const digits: string = nationalDigits(number.text, number.country);
return (
<tr key={number.country}>
<td style={{ padding: "var(--space-xs) var(--space-md)", color: "var(--text-dim)" }}>{number.country}</td>
<td style={{ padding: "var(--space-xs) var(--space-md)" }}>{number.text}</td>
<td style={{ padding: "var(--space-xs) var(--space-md)" }}>{formatNational(digits, number.country)}</td>
<td style={{ padding: "var(--space-xs) var(--space-md)" }}>{toE164(number.country, digits)}</td>
</tr>
);
})}
</tbody>
</table>
);
}
A number's kind
Code
import { PhoneInput } from "@bosunkit/components/phone-input";
import type { ReactElement } from "react";
export const description: string =
"A whole number names its kind and the country it belongs to under the field, here a Dutch mobile.";
export default function Kind(): ReactElement {
return <PhoneInput label="Phone" defaultValue="+31612345678" />;
}
Invalid
Code
import { PhoneInput } from "@bosunkit/components/phone-input";
import type { ReactElement } from "react";
export const description: string =
"An E.164 number has at most 15 digits with its calling code; past that the field says how many it has.";
export default function Invalid(): ReactElement {
return (
<PhoneInput
label="Phone"
defaultValue="+4477009001234567"
error="A phone number has at most 15 digits with the country code; this one has 16. Check the number."
/>
);
}
Read-only
Code
import { PhoneInput } from "@bosunkit/components/phone-input";
import type { ReactElement } from "react";
export const description: string = "A read-only number: the country button does not open.";
export default function ReadOnly(): ReactElement {
return <PhoneInput label="Marina" defaultValue="+351912345678" readOnly />;
}
Disabled
Code
import { PhoneInput } from "@bosunkit/components/phone-input";
import type { ReactElement } from "react";
export const description: string = "A disabled phone field.";
export default function Disabled(): ReactElement {
return <PhoneInput label="Phone" defaultValue="+31612345678" disabled />;
}
Inside a form field
Code
import { FormField } from "@bosunkit/components/form-field";
import { PhoneInput } from "@bosunkit/components/phone-input";
import type { ReactElement } from "react";
export const description: string = "Inside a form field, named by `aria-label`.";
export default function InFormField(): ReactElement {
return (
<FormField label="Shore contact">
<PhoneInput aria-label="Shore contact phone" defaultCountry="DE" />
</FormField>
);
}
Paste
The paste button, at the end of the field, sets the country and the number. It reads:
- a
tel:link, its;ext=as the extension and its other parameters left out; - a number in international form, with
+or00, and the(0)some write after the calling code; - a national number with a label or other text around it, in the field's country, never a date, a time or a vessel's MMSI or IMO number beside it;
- an extension after the number, as each of the six languages marks one: ext, x, extensión, Durchwahl, poste, interno, toestel.
With showExtension a pasted extension fills its input; without it, the field takes the number and its status line
says the extension is not saved.
An international number brings its country; among countries sharing a calling code, the field's own wins. The paste button shows only when the clipboard holds something the field can take, named by what it would paste (Paste 235 012 345). To know that, the field reads the clipboard when it takes focus and when the window comes back, and only where the browser lets the page read it without asking: the clipboard-read permission is granted. Where the browser would ask first, or does not know the permission (Firefox, Safari), the field reads nothing and shows no button, so no prompt appears on focus; Ctrl+V and a long press on a phone paste through the same reader. A read-only or disabled field shows no paste button.
- tel:+31612345678
- 0044 (0)7700 900123
- Mobile: 07700 900123
- tel:+33-6-39-98-12-34;ext=2
- Hello
Code
import { PhoneInput } from "@bosunkit/components/phone-input";
import type { ReactElement } from "react";
export const description: string =
"Copy one of these lines, then focus the field: where the browser lets the page read the clipboard, a paste button named by the number appears; Ctrl+V works everywhere. An international number brings its country, and a national one stays in the field's country.";
const SAMPLES: readonly string[] = [
"tel:+31612345678",
"0044 (0)7700 900123",
"Mobile: 07700 900123",
"tel:+33-6-39-98-12-34;ext=2",
"Hello",
];
export default function Paste(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 520, display: "grid", gap: "var(--space-md)" }}>
<PhoneInput label="Phone" defaultCountry="GB" />
<ul style={{ margin: 0, paddingLeft: "var(--space-lg)", fontFamily: "var(--font-mono)", fontSize: 12 }}>
{SAMPLES.map((sample: string): ReactElement => (
<li key={sample} style={{ userSelect: "all", overflowWrap: "anywhere" }}>
{sample}
</li>
))}
</ul>
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | undefined | – | The field's name on the outline, translated by the caller; omit it inside a FormField. |
| value | string | undefined | – | The number in E.164 form (+447700900123), "" for none. |
| defaultValue | string | undefined | – | The number it starts with when the caller does not hold value. |
| onValueChange | ((value: string) => void) | undefined | – | Called with the number in E.164 form as the crew type, "" while the national number is empty. |
| defaultCountry | string | undefined | "GB" | The country the field starts in, ISO 3166-1 alpha-2: the vessel's flag or home port, which the caller knows. |
| hint | string | undefined | – | Text under the field, translated by the caller. |
| error | string | undefined | – | The caller's own error, shown in place of the field's. |
| required | boolean | undefined | – | Whether the field must hold a number. |
| disabled | boolean | undefined | – | Whether the field takes no input. |
| readOnly | boolean | undefined | – | Whether the field shows its number without letting the crew change it. |
| id | string | undefined | – | The national number input's id, for a Label's htmlFor. |
| name | string | undefined | – | The form field name; the form receives the number in E.164 form. |
| showExtension | boolean | undefined | false | Whether the field has an extension input after the number, for a number that reaches a switchboard (a marina office, an agent, a yard). E.164 has no extension, so it is reported apart from the number; off by default. |
| extension | string | undefined | – | The extension's digits, "" for none, with showExtension. |
| defaultExtension | string | undefined | – | The extension it starts with when the caller does not hold extension. |
| onExtensionChange | ((extension: string) => void) | undefined | – | Called with the extension's digits as the crew type or paste them. |
| extensionName | string | undefined | – | The form field name the extension's digits are sent under. |
| placeholder | string | undefined | – | Text shown while the national number is empty and focused, translated by the caller. |
| aria-label | string | undefined | – | Names the number input when there is no label. |
| className | string | undefined | – | Classes added to the field's root. |
| testId | string | undefined | – | data-testid on the field. |
| ref | Ref<HTMLInputElement> | undefined | – | The national number input. |
| motion | MotionProp | undefined | – | false opens the country list at once and keeps the error line still; true also moves the error line. |
Related
- Outlined field: the box every maritime input draws in.
- Email input: the other contact field of a crew member.
- Crew picker: a crew member's details, phone included, filled from a saved record.