# Phone input

Package: @commenda/pathfinder@0.1.0
Page: /docs/components/phone-input
Figma: https://www.figma.com/design/dn7BkGYEfIyP0o2zfesfVh/The-System?node-id=284-2

```tsx
import { PhoneInput } from '@commenda/pathfinder/phone-input';

export const PhoneInputDefault = () => (
  <PhoneInput
    label="Phone number"
    defaultValue={{ country: 'US', number: '' }}
    placeholder="(201) 555-0123"
    description="Include the area code. We format it when you leave the field."
  />
);
```


## Installation [#installation]

Import the component in your screen:

```tsx
import { PhoneInput } from '@commenda/pathfinder/phone-input';
```

Load the stylesheet once in your app root, after host styles:

```tsx
import '@commenda/pathfinder/styles.css';
```

Pathfinder ships precompiled CSS; no Tailwind setup is required. In Next.js App Router, put interactive state and callbacks in a client component. See [installation](/docs/getting-started/installation) for the workspace setup.

Country artwork loads through an isolated lazy module; other field entry points do not import phone metadata or flags.

## API reference [#api-reference]

| Prop | Type | Requirement | Behavior |
| --- | --- | --- | --- |
| `value` | `PhoneValue` | conditional | One is required; the app supplies its initial jurisdiction |
| `defaultValue` | `PhoneValue` | conditional | One is required; the app supplies its initial jurisdiction |
| `onValueChange` | `((value: PhoneValue) => void)` | conditional | Required in controlled mode; receives the editable text |
| `countryConfig` | `{ allowedCountries?: readonly CountryCode[]; }` | optional | All metadata-supported countries unless constrained |
| `onValidationChange` | `((details: PhoneValueDetails) => void)` | optional | Normalized details when validation changes |
| `label` | `string` | conditional | Visible field label. Supply an accessible name when it is omitted. |
| `hideLabel` | `boolean` | optional | Visually hide the label while preserving its accessible name. |
| `aria-label` | `string` | conditional | Accessible name when the field has no visible label. |
| `required` | `boolean` | conditional | Mark the field as required. See validation guidance for the component. |
| `labelConfig` | `{ optionalLabel: string; help?: never; } \| { help: string; optionalLabel?: never; }` | optional | Choose either `optionalLabel` or `help`, never both. `never` excludes the other field. |
| `description` | `string` | optional | Supporting guidance associated with the field. |
| `errorConfig` | `{ message: string; reveal?: "after-blur" \| "immediate"; }` | optional | Validation message and when it becomes visible. |
| `disableConfig` | `DisableConfig` | optional | Disabled state with an optional explanation. |

### value fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `country` | `CountryCode` | Yes |  |
| `number` | `string` | Yes |  |

### defaultValue fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `country` | `CountryCode` | Yes |  |
| `number` | `string` | Yes |  |

### countryConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `allowedCountries` | `readonly CountryCode[]` | No |  |

### labelConfig fields

Choose one shape; excluded fields cannot be combined.

#### Option 1

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `optionalLabel` | `string` | Yes |  |

Do not supply: `help`.

#### Option 2

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `help` | `string` | Yes |  |

Do not supply: `optionalLabel`.

### errorConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `message` | `string` | Yes |  |
| `reveal` | `"after-blur" \| "immediate"` | No |  |

### disableConfig fields

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `isDisabled` | `boolean` | Yes |  |
| `reason` | `string` | No |  |

Shared presentation, native input attributes, and `readOnly` follow [Input](/docs/components/input). Disabled fields do not submit. Read-only values remain copyable and valid values submit. `CountryCode` is a phone metadata type, with no application enum dependency.

## Countries [#countries]


```tsx
import { PhoneInput } from '@commenda/pathfinder/phone-input';

export const PhoneInputCountries = () => (
  <PhoneInput
    label="Contact number"
    defaultValue={{ country: 'GB', number: '' }}
    countryConfig={{ allowedCountries: ['GB', 'DE', 'IN', 'SG', 'US'] }}
    description="The application supplies the allowed countries and initial jurisdiction."
  />
);
```


Search by country name or calling code. Switching country preserves entered text for review. International input updates country only when metadata resolves it unambiguously; a partial shared dial code such as `+1` does not guess US or Canada.

## Validation and submission [#validation-and-submission]


```tsx
import { useState } from 'react';

import {
  PhoneInput,
  type PhoneValueDetails,
} from '@commenda/pathfinder/phone-input';

export const PhoneInputValidation = () => {
  const [details, setDetails] = useState<PhoneValueDetails>({
    isValid: false,
    e164: null,
  });

  return (
    <div style={{ display: 'grid', gap: 12 }}>
      <PhoneInput
        label="Phone number"
        defaultValue={{ country: 'US', number: '' }}
        onValidationChange={setDetails}
      />
      <output style={{ fontSize: 12 }}>
        {details.e164 ?? 'Enter a valid number to see its E.164 value.'}
      </output>
    </div>
  );
};
```


Typing preserves spaces, brackets, dashes, and incomplete text. Valid numbers format nationally on blur. `onValidationChange` supplies E.164 separately; store the draft and canonical value according to application needs. E.164 excludes extensions; if your workflow uses extensions, retain the draft or collect an extension separately. Phone metadata validity is not proof that a number exists or belongs to a person; ownership verification is application-owned.

## States [#states]


```tsx
import { PhoneInput } from '@commenda/pathfinder/phone-input';

export const PhoneInputStates = () => (
  <div style={{ display: 'grid', gap: 20 }}>
    <PhoneInput
      label="Read-only number"
      defaultValue={{ country: 'US', number: '2015550123' }}
      readOnly
    />
    <PhoneInput
      label="Disabled number"
      defaultValue={{ country: 'US', number: '2015550123' }}
      disableConfig={{ isDisabled: true }}
    />
    <PhoneInput
      label="Required number"
      defaultValue={{ country: 'US', number: '' }}
      required
      errorConfig={{
        message: 'Enter a contact phone number.',
        reveal: 'immediate',
      }}
    />
  </div>
);
```


Read [composite field rules](/docs/components/phone-input#design-rules).

## Design rules [#design-rules]

Figma guidance and reviewed implementation decisions for this component. Rule IDs remain stable for source comments and AI references.

### Phones [#phones]

- **PHONE-001:** One 28px border/focus ring, 16px country flag, 13px dial code, 12px chevron, and searchable country selector. Center artwork in flex containers, never through inline SVG baselines.
- **PHONE-002:** Preserve editable text; format valid numbers nationally on blur. Validate with libphonenumber metadata and expose E.164 separately. The app owns verification and storage.
- **PHONE-003:** The application must supply its jurisdiction. Resolve international input only when unambiguous; do not infer a country from shared partial dial codes or browser locale.
- **PHONE-004:** Country switches preserve typed text for review. Allowed-country policy comes from the application, and restricted countries cannot yield a valid canonical form value.

### Shared rules [#shared-rules]

Labels activate the first input. Descriptions, disabled reasons, and validation messages stay associated with the field. Error icons center on the first 18px text line, including wrapped messages. Controlled values require callbacks; uncontrolled form resets restore defaults. Public props expose application values, never private primitive or calendar objects. Styles ship precompiled for React 18/19 and Next.js consumers.
