Phone Input
Phone number input with country combobox.
Use Phone Input anywhere Cal collects a telephone number. It owns country selection and display
formatting; the consuming form owns required and validity messages. Wrap it in Field and
FieldLabel, matching the label's htmlFor to the input id, so the telephone input has a
visible, accessible name. The composite keeps that label on the telephone textbox rather than the
adjacent country combobox.
The public booking form is the reference implementation. New phone fields should match its value shape, country-selection behavior, required handling, validation timing, and inline errors unless a documented product requirement calls for different behavior.
import { PhoneInput } from "@coss/ui/shared/phone-input";Example
Composition matches a real product call site. Copy is paraphrased.
Canonical composition
Keep the responsibilities separate instead of wrapping product behavior into another input:
| Layer | Responsibility |
|---|---|
PhoneInput | Country selection, display formatting, and emitting complete or incomplete international values |
Consuming Field | Accessible label, required state, touched/invalid state, and inline error copy |
| Form schema | Normalize and validate with getPhoneNumberInputValidation when Booker semantics are required |
| Product surface | Decide whether an empty field should receive a detected defaultCountry |
const defaultCountry = useDefaultPhoneCountry();
<Field name={name} invalid={invalid} touched={isTouched} dirty={isDirty}>
<FieldLabel htmlFor={name}>{t("phone_number")}</FieldLabel>
<PhoneInput
id={name}
name={name}
value={value}
onBlur={onBlur}
onChange={onChange}
defaultCountry={defaultCountry}
/>
<FieldError match={!!error}>{error?.message}</FieldError>
</Field>;This shape follows the public Booker and routing-form fields. A surface using native constraint
validation may use inputRef and required; an RHF/Zod form should keep validation in its form
schema, following the Forms guide.
Validation
PhoneInput emits the current international value, including incomplete input while someone is
typing. It does not decide whether a complete number is acceptable; that policy belongs to the
consuming form.
Booking and routing forms pass the emitted value through getPhoneNumberInputValidation, giving
them the same three outcomes:
| Outcome | Example | Form behavior |
|---|---|---|
| Valid | +1 202 555 0123 | Normalize to E.164 (+12025550123) |
| Invalid | +1 202 555 | Keep the value and show the inline phone-number error |
| Empty after normalization | A bare calling code such as +1 | Apply optional or required-field behavior |
A bare calling code such as +1, or an incomplete supported prefix such as +4, behaves as empty
input. An impossible code such as +999, a number that is clearly too short, or a number outside
the installed metadata's allocated ranges is invalid.
The globe means that no country has been resolved; it can represent incomplete or invalid input,
but also a valid non-geographic number such as +800 1234 5678. Do not use the globe itself as a
validation signal.
Do not show an error while someone is entering an incomplete number. Once validation has surfaced an error, clear it as soon as the corrected value satisfies that form's policy.
Country selection
defaultCountryis optional. Supply it when an empty field should accept a national number in a detected or product-known country. Public booking and routing forms supply the same IP/locale-derived value.- Do not add country detection inside
PhoneInput. App surfaces that need it useuseDefaultPhoneCountry; the shared composite remains independent of app data fetching. - Omit
defaultCountrywhen the existing US fallback is appropriate or when the field always receives an international value. Pass a product-owned fixed country explicitly when it should be the national-number context. - A country parsed from a controlled international
valuetakes precedence overdefaultCountry, so editing an existing E.164 value does not require deriving a default from it. - Entering a recognized international prefix selects the matching country. An unknown or non-geographic prefix leaves the globe selected without opening the picker.
- When countries share an exact calling code, entering that bare code keeps the selected country
when compatible, otherwise it prefers the detected/default country. For example, an Italian
default resolves
+39to Italy rather than leaving the selector in the international state. - The selector stays compact and flag-only. For resolved countries, a non-editable calling-code addon appears between the selector and the telephone input. The international state uses the globe and omits the addon.
- Country names follow the application's language setting from the document
langattribute. Country search matches that name, the two-letter ISO code, or calling code. For example,United,GB, and+44can all find the United Kingdom. - Selecting a country returns focus to the telephone input.
Use inputRef when form infrastructure needs the native telephone input for focus management or
constraint validation. The ref is populated only while the component is mounted.
API
| Prop | Type | Notes |
|---|---|---|
value | string | Controlled international phone value |
onChange | (value: string) => void | Receives valid or incomplete input while typing |
defaultCountry | Country | Initial country when the value does not identify one |
countrySelectLabel, internationalLabel | string | Localizable accessible country-selector labels |
searchPlaceholder, emptyLabel | string | Localizable country-search copy |
onCountryChange | (country?: Country) => void | Runs when a country is selected or the input returns to international state |
inputRef | React.Ref<HTMLInputElement> | Native telephone input for form integration |
required, disabled, name, id | Native input props | Passed to the telephone input |
Call sites
| File | Notes |
|---|---|
apps/web/modules/form-builder/components/Components.tsx | Reference public booking-form field |
packages/features/routing/components/react-awesome-query-builder/widgets.tsx | Public routing-form field |
apps/web/modules/event-types/components/locations/LocationInput.tsx | Event-type phone location |
apps/web/modules/event-types/components/locations/HostLocations.tsx | Per-host phone location |
apps/web/modules/event-types/components/tabs/ai/AIEventController.tsx | AI phone configuration |
apps/web/components/dialog/EditLocationDialog.tsx | Booking location editing |
apps/web/modules/ee/workflows/components/canvas/node-config-sheet.tsx | Workflow step configuration |
apps/web/modules/ee/workflows/components/TestPhoneCallDialog.tsx | Cal.ai test call |
Example URLs
Not exhaustive — a few places you can open locally (e.g. http://localhost:3000) to see this in product.
| Path | Notes |
|---|---|
/event-types/<eventTypeId>?tabName=setup | Phone location input |
/event-types/<eventTypeId>?tabName=bookingForm | Configure the public booking-form field |
/event-types/<eventTypeId>?tabName=ai | AI phone configuration |
/bookings/<status> | Edit a booking's phone location in a dialog |
/workflows/<workflowId> | Canvas step configuration and test-call flows |
/<user>/<event-slug> | Public booking form when a phone field is configured |
/forms/<formId> | Public routing form when a phone field is configured |