Skip to content

PhoneInput

Component that is used to get and validate phone number input from users with country selection.

Phone Input is a specialized text input component built for capturing and validating phone numbers. It features integrated country code selection using libphonenumber-js for accurate international phone number validation.

PhoneInput is headless: state, formatting and validation live in the usePhoneInput hook. Call the hook, spread inputProps onto <PhoneInput>, and wrap it in a FormControl you own (spread formControlProps) so you can add your own label, required indicator, and helper text. The component renders its built-in error message into that FormControl.

For the complete list of hook options and return values, see the API reference.

Call usePhoneInput, then spread formControlProps onto a FormControl and inputProps onto PhoneInput. The country defaults to the device locale (falling back to the locale region) and the number is formatted and validated on blur.

import { FormControl, FormLabel, PhoneInput, usePhoneInput } from "@caspeco/casper-ui-library";
export default function Demo() {
const phone = usePhoneInput({ defaultCountry: "SE" });
return (
<FormControl {...phone.formControlProps}>
<FormLabel>Phone number</FormLabel>
<PhoneInput {...phone.inputProps} />
</FormControl>
);
}

The hook keeps two views of the number, and you almost always want the second one for storage:

  • phone.value is the display value, exactly what the user sees.
  • phone.e164 is the canonical value (e.g. +46771793336) to store or submit.
  • phone.country is the selected CountryCode.
  • phone.isValid reflects whether the current number is valid (independent of whether an error is shown).

Type into the field below and watch the display value and E.164 diverge.

import { FormControl, FormLabel, PhoneInput, Stack, Text, usePhoneInput } from "@caspeco/casper-ui-library";
export default function Demo() {
const phone = usePhoneInput({ defaultValue: "+46 77 179 33 36" });
return (
<Stack w="300px">
<FormControl {...phone.formControlProps}>
<FormLabel>Phone number</FormLabel>
<PhoneInput {...phone.inputProps} />
</FormControl>
<Text>Display value: {phone.value || "(empty)"}</Text>
<Text>E.164: {phone.e164 || "(empty)"}</Text>
<Text>Country: {phone.country}</Text>
<Text>Is valid: {phone.isValid ? "Yes" : "No"}</Text>
</Stack>
);
}

Seed the field with defaultCountry (just the flag/dial code) or defaultValue (a full existing number, the country is inferred from its + prefix).

If omitted, the country defaults to the device locale (falling back to the locale region) and the number is empty.

  • phone.setValue can be used to programmatically change the number after mount.
  • phone.reset restores the initial value and country.
import { Button, FormControl, FormLabel, PhoneInput, Stack, usePhoneInput } from "@caspeco/casper-ui-library";
export default function Demo() {
const phone = usePhoneInput({ defaultValue: "+46 77 179 33 36" });
return (
<Stack w="300px">
<FormControl {...phone.formControlProps}>
<FormLabel>Phone number</FormLabel>
<PhoneInput {...phone.inputProps} />
</FormControl>
<Button onClick={() => phone.setValue("+1 202 555 0136")}>Load US number</Button>
<Button onClick={() => phone.reset()}>Reset</Button>
</Stack>
);
}

isRequired lives on the hook and is forwarded to the FormControl via formControlProps. Call phone.validate() in your submit handler, it reveals the built-in errors and returns a fresh result you can use to gate submission. Use phone.isValid for live state such as enabling a button.

import {
Button,
FormControl,
FormLabel,
PhoneInput,
Stack,
Text,
ThemeColor,
usePhoneInput,
} from "@caspeco/casper-ui-library";
import { useState } from "react";
export default function Demo() {
const phone = usePhoneInput({ defaultCountry: "SE", isRequired: true });
const [saved, setSaved] = useState<string | null>(null);
function handleSave() {
const { isValid, errorMessage } = phone.validate();
if (!isValid) {
console.log("Validation failed:", errorMessage);
setSaved(null);
return;
}
setSaved(phone.e164);
}
function handleClear() {
phone.reset();
setSaved(null);
}
return (
<Stack>
<FormControl {...phone.formControlProps}>
<FormLabel>Phone number</FormLabel>
<PhoneInput {...phone.inputProps} placeholder="Enter phone number" />
</FormControl>
{saved && <Text color={ThemeColor.Success}>✓ Saved: {saved}</Text>}
<Button onClick={handleSave} variant={phone.isValid ? "primary" : "secondary"}>
Save phone number
</Button>
<Button onClick={handleClear}>Clear</Button>
</Stack>
);
}

Pass a customValidator to usePhoneInput to add your own rule. It receives the E.164 value, runs once the number is otherwise valid, and returns an error message (or undefined to pass). The hook surfaces it on blur and wires it into formControlProps.isInvalid and the rendered error message automatically.

import { FormControl, FormLabel, PhoneInput, Stack, Text, usePhoneInput } from "@caspeco/casper-ui-library";
const takenNumbers = ["+46771793336", "+3725555555"];
export default function Demo() {
const phone = usePhoneInput({
defaultCountry: "SE",
customValidator: (e164) => (takenNumbers.includes(e164) ? "This number is already taken" : undefined),
});
return (
<Stack>
<div>
<Text>Taken numbers:</Text>
{takenNumbers.map((number) => (
<Text key={number}>{number}</Text>
))}
</div>
<FormControl {...phone.formControlProps}>
<FormLabel>Phone number</FormLabel>
<PhoneInput {...phone.inputProps} placeholder="Must be unique" />
</FormControl>
</Stack>
);
}

Disable or lock the field from the FormControl you own. Set isDisabled or isReadOnly on it, exactly as you would for any other input. PhoneInput inherits the state (and disables the country selector too) from the surrounding FormControl’s context.

import { FormControl, FormLabel, PhoneInput, Stack, usePhoneInput } from "@caspeco/casper-ui-library";
function DisabledField() {
const phone = usePhoneInput({ defaultValue: "+46 77 179 33 36" });
return (
<FormControl {...phone.formControlProps} isDisabled>
<FormLabel>Disabled</FormLabel>
<PhoneInput {...phone.inputProps} />
</FormControl>
);
}
function ReadOnlyField() {
const phone = usePhoneInput({ defaultValue: "+46 77 179 33 36" });
return (
<FormControl {...phone.formControlProps} isReadOnly>
<FormLabel>Read-only</FormLabel>
<PhoneInput {...phone.inputProps} />
</FormControl>
);
}
export default function Demo() {
return (
<Stack>
<DisabledField />
<ReadOnlyField />
</Stack>
);
}

PhoneInput supports multiple size options: sm, md (default), lg, and xl. The size prop affects the input field, country selector button, and all associated icons.

import {
FormControl,
FormLabel,
PhoneInput,
Stack,
ThemeSpace,
usePhoneInput,
} from "@caspeco/casper-ui-library";
function PhoneField({ size, label }: { size: "sm" | "md" | "lg" | "xl"; label: string }) {
const phone = usePhoneInput({ defaultCountry: "SE" });
return (
<FormControl {...phone.formControlProps}>
<FormLabel>{label}</FormLabel>
<PhoneInput {...phone.inputProps} size={size} placeholder="Enter phone number" />
</FormControl>
);
}
export default function Demo() {
return (
<Stack gap={ThemeSpace.Large}>
<PhoneField size="sm" label="Small (sm)" />
<PhoneField size="md" label="Medium (md) - Default" />
<PhoneField size="lg" label="Large (lg)" />
<PhoneField size="xl" label="Extra Large (xl)" />
</Stack>
);
}

PhoneInput supports two visual variants: outline (default) and ghost.

import { FormControl, PhoneInput, Stack, ThemeSpace, usePhoneInput } from "@caspeco/casper-ui-library";
function PhoneField({ variant, placeholder }: { variant: "outline" | "ghost"; placeholder: string }) {
const phone = usePhoneInput({ defaultCountry: "SE" });
return (
<FormControl {...phone.formControlProps}>
<PhoneInput {...phone.inputProps} variant={variant} placeholder={placeholder} />
</FormControl>
);
}
export default function Demo() {
return (
<Stack gap={ThemeSpace.Small}>
<PhoneField variant="outline" placeholder="Outline variant (default)" />
<PhoneField variant="ghost" placeholder="Ghost variant" />
</Stack>
);
}