# International phone field

One phone number with a searchable country list, for every screen that asks for one: sign-in, sign-up, the profile and the address. The number formats for its country as it is typed, a typed +33 picks France, a valid number is shown nationally on blur and leaves as E.164 or not at all, and a list of allowed countries is honoured. phone is a client-only service (DESIGN.md §8): no server, no config file, no route. The list is Base UI's Combobox, so the keyboard, the focus and the ARIA are the primitive's.

Import: @slices.sh/phone/headless

Public parts: 17

## Installation

### 1. Install the service registry

services/index.ts, which `import "@/services"` loads. A provider setup writes the same file; skip this if your app has one.

```bash
npx shadcn@latest add "https://slices.sh/r/services.json"
```

### 2. Install the headless library

```bash
npm install @slices.sh/phone@^0.1.2
```

### 3. Install the service

Install the provider configuration via shadcn or the slices CLI. Skip this step if your app already has the service configured. Provider commands and provisioning: https://slices.sh/docs/services/phone.md

## Anatomy

### Composition

```text
Phone.Root
  Phone.Label
  Phone.Country
    Phone.CountryTrigger
      Phone.CountryFlag
    Phone.CountryPopup
      Phone.CountrySearch
      Phone.CountryList
        Phone.Countries
          Phone.CountryItem
            Phone.CountryName
            Phone.CountryCallingCode
        Phone.CountryEmpty
  Phone.Input
  Phone.Invalid
  Phone.Blocked
  Phone.Value name="phone"
```

## A phone field

```tsx
"use client"

import { Phone } from "@slices.sh/phone/headless"

export function Example() {
  return (
    <Phone.Root defaultCountry="FR">
      <Phone.Label>Phone number</Phone.Label>
      <Phone.Input />
      <Phone.Value name="phone" />
    </Phone.Root>
  )
}

```

Formatting and validation are local. Value submits the normalized phone number with your form.

## Limit countries

```tsx
"use client"

import { Phone } from "@slices.sh/phone/headless"

export function Example() {
  return (
    <Phone.Root defaultCountry="FR" allowedCountries={["FR", "BE"]}>
      <Phone.Label>Mobile</Phone.Label>
      <Phone.Input />
      <Phone.Blocked>Choose a French or Belgian number.</Phone.Blocked>
      <Phone.Invalid>Check the number.</Phone.Invalid>
    </Phone.Root>
  )
}

```

The gates show only the relevant validation message.

## Complete API reference

### Phone.Root

scope · Generic

One phone number: its text, its country and the countries on offer. Renders nothing itself.

Import: @slices.sh/phone/headless

```tsx
PhoneRootProps

type PhoneRootProps = RootProps & {
  /**
   * Phone has no service to be missing, so nothing here throws. `false` is off: the root
   * renders nothing, and so does every part of it. Anything else leaves the field on.
   */
  enabled?: Enabled
  /** Accepted so every root takes the same props: phone has no configs. */
  config?: string
  /** The E.164 number, when something else keeps it. Without it the field keeps its own. */
  value?: string | null
  defaultValue?: string
  /** E.164 once the number is valid and its country allowed, `null` otherwise. */
  onValueChange?(number: string | null): void
  /** Where a national number is read until a country is picked or a `+` typed. ISO 3166-1 alpha-2. */
  defaultCountry?: string
  /** The countries a number may be in, which are also the ones the list offers. Without it, every country. */
  allowedCountries?: readonly string[]
  /** The language the countries are named and sorted in. English unless the app says otherwise. */
  locale?: string
  /** The number input's id, for a form that already points a label and a refused-field focus at one. */
  id?: string
}
```

Source: @slices.sh/phone/src/headless/root.tsx

### Phone.Label

content · Generic · Element: label

The number itself. Input is <input type="tel" inputmode="tel" autocomplete="tel"> with value, id and aria-invalid locked; children get { invalid, status }, and data-state is empty, incomplete, invalid, blocked or valid. invalidMessage is the form's own validity message, and refused marks it invalid for a refusal the number's reading cannot know, such as a backend that will not text that country. Value is a hidden input: give it a name and it posts the E.164 number, or an empty string. Label locks htmlFor.

Import: @slices.sh/phone/headless

Phone.Label must be inside <Phone.Root>.

```tsx
PartProps<"label">
```

Source: @slices.sh/phone/src/headless/label.tsx

### Phone.Country

scope · Generic

The country list: a button that opens a popup with its own search box.

Renders no element, like every root. The list and its search are the parts inside; the
keyboard, the focus and the ARIA of a combobox are Base UI's.

Import: @slices.sh/phone/headless

Phone.Country must be inside <Phone.Root>.

```tsx
RootProps
```

Source: @slices.sh/phone/src/headless/country.tsx

### Phone.CountryTrigger

action · Generic · Element: button

The button that opens the list. It shows the flag until its children say otherwise.

Import: @slices.sh/phone/headless

Phone.CountryTrigger must be inside <Phone.Root>.

```tsx
PartProps<"button", TriggerState>

type TriggerState = { country: PhoneCountry | null }
```

Source: @slices.sh/phone/src/headless/country-trigger.tsx

### Phone.CountryFlag

content · Generic

The flag, as text. Inside an option it is that option's, elsewhere the number's country.

Import: @slices.sh/phone/headless

```tsx
PartProps<null>
```

Source: @slices.sh/phone/src/headless/country-flag.tsx

### Phone.CountryPopup

content · Generic · Element: div

Where the list appears: a positioned layer under the button, which flips when there is no
room and publishes `--anchor-width` and `--available-height` for the skin to size itself by.

Closing hands focus to the number rather than back to the button, because somebody who
has picked a country is about to type the number in it.

Import: @slices.sh/phone/headless

Phone.CountryPopup must be inside <Phone.Root>.

```tsx
PartProps<"div">
```

Source: @slices.sh/phone/src/headless/country-popup.tsx

### Phone.CountrySearch

input · Generic · Element: input

The box that narrows the list: a country's name, its code, or its calling code.

Import: @slices.sh/phone/headless

```tsx
PartProps<"input">
```

Source: @slices.sh/phone/src/headless/country-search.tsx

### Phone.CountryList

content · Generic · Element: div

The options. Its element comes from the caller, and `role="listbox"` is the primitive's.

Import: @slices.sh/phone/headless

```tsx
PartProps<"div">
```

Source: @slices.sh/phone/src/headless/country-list.tsx

### Phone.Countries

scope · Generic

Its children once for each country the search leaves, each in that country's scope.

Import: @slices.sh/phone/headless

Phone.Countries must be inside <Phone.Country>.

```tsx
RootProps
```

Source: @slices.sh/phone/src/headless/countries.tsx

### Phone.CountryItem

content · Generic · Element: div

One option. `role="option"`, the highlight and `aria-selected` are the primitive's, and
choosing it is too: the root hears the value change.

Import: @slices.sh/phone/headless

Phone.CountryItem must be inside <Phone.Countries>.

Phone.CountryItem must be inside <Phone.Root>.

```tsx
PartProps<"div", ItemState>

type ItemState = { country: PhoneCountry; selected: boolean }
```

Source: @slices.sh/phone/src/headless/country-item.tsx

### Phone.CountryName

content · Generic

The country's name in the root's locale. Inside an option it is that option's, elsewhere the number's country.

Import: @slices.sh/phone/headless

```tsx
PartProps<null>
```

Source: @slices.sh/phone/src/headless/country-name.tsx

### Phone.CountryCallingCode

content · Generic

`+33`. Inside an option it is that option's, elsewhere the number's country.

Import: @slices.sh/phone/headless

```tsx
PartProps<null>
```

Source: @slices.sh/phone/src/headless/country-calling-code.tsx

### Phone.CountryEmpty

content · Generic · Element: div

Shown only while the search leaves no country. The primitive announces it.

Import: @slices.sh/phone/headless

```tsx
PartProps<"div">
```

Source: @slices.sh/phone/src/headless/country-empty.tsx

### Phone.Input

input · Generic · Element: input

The number, formatted for its country as it is typed.

`invalidMessage` is what a form's own validation says when the number is not one, and
is what stops a submit. It is a prop because the words are the pattern's, not this part's.

`refused` is a refusal that is not the number's own reading: a form that would not take it,
or a backend that would not text it. It marks the field invalid for as long as it is set.

Import: @slices.sh/phone/headless

Phone.Input must be inside <Phone.Root>.

```tsx
PartProps<"input", InputState> & { invalidMessage?: string; refused?: boolean }

type InputState = { invalid: boolean; status: PhoneStatus }
```

Source: @slices.sh/phone/src/headless/input.tsx

### Phone.Invalid

gate · Generic

The number is wrong, or unfinished, and the person has left it. Which of the two is the
`status` its children receive. A country that is not allowed is `Phone.Blocked`'s.

Import: @slices.sh/phone/headless

Phone.Invalid must be inside <Phone.Root>.

```tsx
GateProps<{ status: PhoneStatus }>
```

Source: @slices.sh/phone/src/headless/invalid.tsx

### Phone.Blocked

gate · Generic

The number is in a country the root does not allow. Said as soon as its prefix shows which.

Import: @slices.sh/phone/headless

Phone.Blocked must be inside <Phone.Root>.

```tsx
GateProps<{ country: PhoneCountry | null }>
```

Source: @slices.sh/phone/src/headless/blocked.tsx

### Phone.Value

input · Generic · Element: input

The number a form submits: E.164, or nothing while the number is not one.

Import: @slices.sh/phone/headless

Phone.Value must be inside <Phone.Root>.

```tsx
PartProps<"input">
```

Source: @slices.sh/phone/src/headless/value.tsx
