A native dialing-code selector paired with a telephone field that preserves the user’s formatting.
Contact number
We will only use this for account recovery.
import { PhoneInput } from "@/components/ballmac/phone-input"
export default function PhoneInputDemo() {
return (
<div className="w-full max-w-sm rounded-xl border bg-card p-5 shadow-sm">
<p className="text-sm font-semibold">Contact number</p>
<p className="mt-1 mb-4 text-xs text-muted-foreground">
We will only use this for account recovery.
</p>
<PhoneInput
label="Contact number"
defaultCountryCode="+91"
placeholder="Your phone number"
/>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/phone-inputAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/phone-input.tsx// Ballmac UI: Phone Input. https://ui.ballmac.com/components/phone-input "use client" import * as React from "react" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type PhoneCountry = { code: string; label: string } type PhoneInputProps = Omit< React.ComponentProps<"input">, "type" | "value" | "defaultValue" | "onChange" > & { /** National number, controlled by the parent. Formatting is preserved. */ value?: string /** Initial national number when uncontrolled. */ defaultValue?: string /** Called with the edited national number. */ onValueChange?: (value: string) => void /** Controlled international dialing prefix. */ countryCode?: string /** Initial dialing prefix when uncontrolled. */ defaultCountryCode?: string /** Called when the dialing prefix changes. */ onCountryCodeChange?: (code: string) => void /** Dialing prefixes available in the menu. */ countries?: PhoneCountry[] /** Accessible name of the number field. */ label?: string /** Name of the separate country-code form field. */ countryCodeName?: string } const defaultCountries: PhoneCountry[] = [ { code: "+1", label: "+1 United States" }, { code: "+44", label: "+44 United Kingdom" }, { code: "+91", label: "+91 India" }, { code: "+49", label: "+49 Germany" }, { code: "+61", label: "+61 Australia" }, ] function PhoneInput({ value, defaultValue = "", onValueChange, countryCode, defaultCountryCode = "+1", onCountryCodeChange, countries = defaultCountries, label, countryCodeName, className, disabled, ...props }: PhoneInputProps) { const msg = useMessages() label ??= msg("phone-input.label", "Phone number") const [internal, setInternal] = React.useState(defaultValue) const [internalCode, setInternalCode] = React.useState(defaultCountryCode) const current = value ?? internal const code = countryCode ?? internalCode return ( <div data-slot="phone-input" className={cn( "flex h-9 w-full min-w-0 items-center rounded-md border border-input bg-background shadow-xs transition-[border-color,box-shadow] duration-150 motion-reduce:transition-none focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50", className, )} > <select data-slot="phone-input-country" aria-label={msg("phone-input.countryDialingCode", "Country dialing code")} name={countryCodeName} disabled={disabled} value={code} onChange={(event) => { if (countryCode === undefined) setInternalCode(event.target.value) onCountryCodeChange?.(event.target.value) }} className="h-full max-w-24 shrink-0 rounded-s-md border-e border-input bg-transparent px-2 text-sm text-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50" > {countries.map((country) => ( <option key={country.code} value={country.code}> {country.label} </option> ))} </select> <input data-slot="phone-input-field" type="tel" inputMode="tel" autoComplete="tel-national" aria-label={label} disabled={disabled} value={current} onChange={(event) => { if (value === undefined) setInternal(event.currentTarget.value) onValueChange?.(event.currentTarget.value) }} className="h-full min-w-0 flex-1 bg-transparent px-3 text-sm text-foreground outline-none placeholder:text-muted-foreground disabled:opacity-50" {...props} /> </div> ) } export { PhoneInput, type PhoneInputProps, type PhoneCountry }Update the import paths to match your project setup.
Usage
import { PhoneInput } from "@/components/ballmac/phone-input"The full example is in the Code tab above.
Examples
States and variants
International contact
import { PhoneInput } from "@/components/ballmac/phone-input"
export default function PhoneInputStates() {
return (
<div className="w-full max-w-sm">
<p className="mb-2 text-sm font-medium">International contact</p>
<PhoneInput
label="International contact"
defaultCountryCode="+44"
placeholder="National number"
/>
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
valueNational number, controlled by the parent. Formatting is preserved. | string | — |
defaultValueInitial national number when uncontrolled. | string | "" |
onValueChangeCalled with the edited national number. | (value: string) => void | — |
countryCodeControlled international dialing prefix. | string | — |
defaultCountryCodeInitial dialing prefix when uncontrolled. | string | "+1" |
onCountryCodeChangeCalled when the dialing prefix changes. | (code: string) => void | — |
countriesDialing prefixes available in the menu. | PhoneCountry[] | defaultCountries |
labelAccessible name of the number field. | string | — |
countryCodeNameName of the separate country-code form field. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Arrow Up / Down | Changes the native dialing-code selection |
| Tab | Moves to the telephone field |
Use with AI
A native dialing-code selector paired with a telephone field that preserves the user’s formatting. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Phone Input (@ballmac/phone-input) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Collect a contact number with a dialing prefix
- Edit a national number without forced formatting
Not for
- Use input type tel when a country selector is unnecessary
Registry JSON: https://ui.ballmac.com/r/phone-input.json
Credits
Free to use in personal and commercial projects.
- npm
- None
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Input
A text input in three heights that match Button, plus InputGroup and InputGroupAddon for leading or trailing icons, units and domains.
Calendar
A date grid for one day, several days or a range, with month and year selects, week numbers, disabled rules and range-end styling, on React DayPicker.
Color Picker
A native color chooser paired with an editable hex field and a live swatch.
Combobox
A searchable single-choice select with groups, descriptions, keywords, clearable value, invalid state and hidden-input form submission.