Headless / Input components
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.
17 public parts · 2examples
@slices.sh/phone/headlessView as Markdown ↗Your design. Our behavior.
Explore the tree to see what each part controls. Gates reveal the relevant state; roots outline their scope.
Phone.Root · Generic
Installation
Choose what your component needs. Run these commands in an initialized Next.js app.
Before you start
The steps below run in a Next.js app with shadcn set up (it has a components.json). In an existing Next.js app, such as one from create-next-app, set shadcn up first:
npx shadcn@latest init -t next -b base -p novaOr start fresh: the slices CLI creates the app and sets it up.
npx slices.sh installInstall the headless library
npmAdd the React parts to your app, then compose them with your own elements.
npm install @slices.sh/phone@^0.1.2Import from @slices.sh/phone/headless.
These parts run locally. No service installation or provisioning is needed.
Start small
Parts bring the behavior. Use their default elements, or supply your own with render.
"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.
API reference
Every public part used by this experience is listed below. Gates render children without adding an element. Element parts accept native element props and render; function children receive the state declared in their props. Scopes provide context rather than a visual element.
Phone.Rootscope · Generic
One phone number: its text, its country and the countries on offer. Renders nothing itself.
Props
9 documented props
| Prop | Type | Default |
|---|---|---|
enabledPhone has no service to be missing, so nothing here throws. | Enabled | — |
configAccepted so every root takes the same props: phone has no configs. | string | — |
valueThe E.164 number, when something else keeps it. Without it the field keeps its own. | string | null | — |
defaultValue | string | — |
defaultCountryWhere a national number is read until a country is picked or a | string | — |
allowedCountriesThe countries a number may be in, which are also the ones the list offers. Without it, every country. | readonly string[] | — |
localeThe language the countries are named and sorted in. English unless the app says otherwise. | string | — |
idThe number input's id, for a form that already points a label and a refused-field focus at one. | string | — |
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/root.tsx
Phone.Labelcontent · Generic · <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.
Required scope
Phone.Label must be inside <Phone.Root>.
Props
2 documented props · Native <label> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/label.tsx
Phone.Countryscope · 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.
Required scope
Phone.Country must be inside <Phone.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/country.tsx
Phone.CountryTriggeraction · Generic · <button>
The button that opens the list. It shows the flag until its children say otherwise.
Required scope
Phone.CountryTrigger must be inside <Phone.Root>.
Props
2 documented props · Native <button> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: TriggerState) => ReactNode) | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/country-trigger.tsx
Phone.CountryFlagcontent · Generic
The flag, as text. Inside an option it is that option's, elsewhere the number's country.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/country-flag.tsx
Phone.CountryPopupcontent · Generic · <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.
Required scope
Phone.CountryPopup must be inside <Phone.Root>.
Props
2 documented props · Native <div> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/country-popup.tsx
Phone.CountrySearchinput · Generic · <input>
The box that narrows the list: a country's name, its code, or its calling code.
Props
2 documented props · Native <input> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/country-search.tsx
Phone.CountryListcontent · Generic · <div>
The options. Its element comes from the caller, and `role="listbox"` is the primitive's.
Props
2 documented props · Native <div> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/country-list.tsx
Phone.Countriesscope · Generic
Its children once for each country the search leaves, each in that country's scope.
Required scope
Phone.Countries must be inside <Phone.Country>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/countries.tsx
Phone.CountryItemcontent · Generic · <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.
Required scope
Phone.CountryItem must be inside <Phone.Countries>.
Phone.CountryItem must be inside <Phone.Root>.
Props
2 documented props · Native <div> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: ItemState) => ReactNode) | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/country-item.tsx
Phone.CountryNamecontent · Generic
The country's name in the root's locale. Inside an option it is that option's, elsewhere the number's country.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/country-name.tsx
Phone.CountryCallingCodecontent · Generic
`+33`. Inside an option it is that option's, elsewhere the number's country.
Props
2 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/country-calling-code.tsx
Phone.CountryEmptycontent · Generic · <div>
Shown only while the search leaves no country. The primitive announces it.
Props
2 documented props · Native <div> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/country-empty.tsx
Phone.Inputinput · Generic · <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.
Required scope
Phone.Input must be inside <Phone.Root>.
Props
4 documented props · Native <input> attributes supported
| Prop | Type | Default |
|---|---|---|
invalidMessage | string | "" |
refused | boolean | false |
childrenContent rendered inside this part. | ReactNode | ((state: InputState) => ReactNode) | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/input.tsx
Phone.Invalidgate · 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.
Required scope
Phone.Invalid must be inside <Phone.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { status: PhoneStatus }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/invalid.tsx
Phone.Blockedgate · Generic
The number is in a country the root does not allow. Said as soon as its prefix shows which.
Required scope
Phone.Blocked must be inside <Phone.Root>.
Props
1 documented props
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | ((state: { country: PhoneCountry | null }) => ReactNode) | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/blocked.tsx
Phone.Valueinput · Generic · <input>
The number a form submits: E.164, or nothing while the number is not one.
Required scope
Phone.Value must be inside <Phone.Root>.
Props
2 documented props · Native <input> attributes supported
| Prop | Type | Default |
|---|---|---|
childrenContent rendered inside this part. | ReactNode | — |
renderReplace the default element with your own while retaining the part’s behavior and accessibility. | ReactElement | — |
Required props are marked. A dash means no explicit default is declared in this component.
Source: @slices.sh/phone/src/headless/value.tsx
When something is missing
- No service configuration
- Install the phone setup below and import "@/services". Required service roots report missing setup; gates do not replace that configuration.
- Missing capability or parent scope
- Follow each part’s context requirements and the example composition. Capability-aware controls hide when the provider cannot perform their action.