---
title: Phone Field
description: A form input for collecting phone numbers with automatic formatting, pattern masking, and optional country code selection.
---

```tsx
'use client';

import { PhoneField } from '@/ui/forms/PhoneField';

export function PhoneFieldDemo() {
  return (
    <div className="flex flex-col gap-16">
      <PhoneField
        label="Phone Number"
        description="Enter your phone number"
        placeholder="+33 1 23 45 67 89"
        mode="full"
        className="max-w-sm"
        pattern="+33 # ## ## ## ##"
        id="phonefield-full"
        name="Demo for the full version of the PhoneField component"
      />
      <PhoneField
        label="Phone Number"
        description="Select your code and number"
        placeholder="1 23 45 67 89"
        mode="split"
        className="max-w-sm"
        pattern="# ## ## ## ##"
        id="phonefield-split"
        name="Demo for the split version of the PhoneField component"
      />
    </div>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/phone-field.json
```

Or manually, copy the following code into `/ui/forms/phone-field.tsx`:

```tsx
import { type IconicTypes } from '@clubmed/trident-icons';
import { FormControl, type FormControlProps } from './FormControl';
import { PhoneFieldFullInput, type PhoneFieldFullProps } from './PhoneFieldFullInput';
import { PhoneFieldSplitInput, type PhoneFieldSplitProps } from './PhoneFieldSplitInput';
import { useId } from 'react';

export interface PhoneValue {
  full: string;
  prefix?: string;
  number?: string;
  raw: string;
}

export type PhoneFieldProps<Value = PhoneValue> = FormControlProps<Value> &
  PhoneFieldFullProps &
  PhoneFieldSplitProps & {
    mode?: 'full' | 'split';
    pattern?: string;
    iconType?: IconicTypes;
    placeholder?: string;
  };

export const PhoneField = <Value = PhoneValue,>(props: PhoneFieldProps<Value>) => {
  const {
    id,
    name,
    value,
    label,
    className,
    description,
    mode = 'full',
    dataTestId = 'PhoneField',
    disabled,
    required,
    hideRequiredStar,
    validationStatus,
    errorMessage,
    onChange,
    ...rest
  } = props;
  const onPhoneChange = onChange as ((event: Event, value: PhoneValue) => void) | undefined;
  const internalId = useId();

  const formControlProps = {
    id: id || internalId,
    label,
    className,
    description,
    dataName: 'PhoneField',
    dataTestId,
    disabled,
    required,
    hideRequiredStar,
    validationStatus,
    errorMessage,
  };

  return (
    <FormControl {...formControlProps}>
      {mode === 'full' ? (
        <PhoneFieldFullInput
          id={formControlProps.id}
          name={name}
          value={value as PhoneValue | undefined}
          disabled={disabled}
          required={required}
          validationStatus={validationStatus}
          onChange={onPhoneChange}
          {...rest}
        />
      ) : (
        <PhoneFieldSplitInput
          id={formControlProps.id}
          name={name}
          value={value as PhoneValue | undefined}
          disabled={disabled}
          required={required}
          validationStatus={validationStatus}
          onChange={onPhoneChange}
          {...rest}
        />
      )}
    </FormControl>
  );
};
```

## Usage

### Full Mode

Single input field with pattern-based formatting for consistent phone number display.

```tsx showLineNumbers
'use client';

import { useState } from 'react';
import { PhoneField } from '@/docs/lib/ui/forms/phone-field';

export function Component() {
  const [phone, setPhone] = useState<PhoneValue | null>(null);

  return (
    <PhoneField
      label="Phone Number"
      description="We'll send a verification code to this number"
      mode="full"
      pattern="## ## ## ## ##"
      placeholder="Enter phone number"
      onChange={(_, value) => setPhone(value)}
      required
    />
  );
}
```

### Split Mode

Separate prefix selector and number input for international phone numbers.

```tsx showLineNumbers
'use client';

import { useState } from 'react';
import { PhoneField } from '@/docs/lib/ui/forms/phone-field';

export function Component() {
  const [phone, setPhone] = useState<PhoneValue | null>(null);

  return (
    <PhoneField
      label="International Phone"
      mode="split"
      defaultPrefix="+1"
      pattern="## ## ## ## ##"
      placeholder="Enter phone number"
      onChange={(_, value) => {
        // value.full: "+1 12 34 56 78 90"
        // value.prefix: "+1"
        // value.number: "12 34 56 78 90"
        // value.raw: "11234567890" (for backend)
        setPhone(value);
      }}
    />
  );
}
```

## API Reference

### PhoneField

| Prop             | Type                                      | Default               | Description                                                                                                                |
| ---------------- | ----------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `mode`           | `"full" \| "split"`                       | `"full"`              | Display mode. "full" shows single input with pattern. "split" shows prefix selector + number input.                       |
| `pattern`        | `string`                                  | `"## ## ## ## ##"`    | Format pattern where `#` represents digit placeholder. Example: `"+1 (###) ###-####"` or `"## ## ## ## ##"`.              |
| `prefixes`       | `PhonePrefix[]`                           | `DEFAULT_PREFIXES`    | Array of country code prefixes for split mode. Only used when `mode="split"`.                                             |
| `defaultPrefix`  | `string`                                  | First prefix in array | Default selected country code in split mode. Must match one of the `prefixes` values.                                     |
| `label`          | `string`                                  | `undefined`           | The label text displayed above the input field.                                                                            |
| `description`    | `string`                                  | `undefined`           | Helper text displayed below the label to provide additional context.                                                       |
| `placeholder`    | `string`                                  | `""`                  | Placeholder text shown when the input is empty.                                                                            |
| `id`             | `string`                                  | Auto-generated        | The unique identifier for the input field. Auto-generated if not provided.                                                 |
| `name`           | `string`                                  | Same as `id`          | The name attribute for the input field, used in form submissions. Defaults to the `id` value.                              |
| `value`          | `PhoneValue`                              | `undefined`           | The current phone value. See PhoneValue type below.                                                                        |
| `onChange`       | `(event: Event, value: PhoneValue) => void` | `undefined` | Callback called when the phone number changes. Receives event and a PhoneValue object. |
| `icon`           | `IconicNames`                             | `undefined`           | Optional icon name from @clubmed/trident-icons to display on the left side of the input (full mode only).                 |
| `iconType`       | `IconicTypes`                             | `undefined`           | Optional icon type variant for the icon.                                                                                   |
| `disabled`       | `boolean`                                 | `false`               | Whether the field is disabled. Disabled fields have reduced opacity and cannot be interacted with.                         |
| `required`       | `boolean`                                 | `false`               | Whether the field is required. Displays a red asterisk next to the label.                                                  |
| `hideRequiredStar` | `boolean`                               | `false`               | When `true`, hides the required asterisk even if the field is required.                                                    |
| `validationStatus` | `"default" \| "error" \| "success"`     | `"default"`           | The validation state of the field. Changes border color and shows status icons.                                            |
| `errorMessage`   | `string`                                  | `undefined`           | Error message text displayed when `validationStatus` is `"error"`.                                                         |
| `dataTestId`     | `string`                                  | `"PhoneField"`        | Custom test ID for the component wrapper, useful for testing.                                                              |
| `className`      | `string`                                  | `undefined`           | Additional CSS classes for the component wrapper.                                                                          |

The component also accepts all standard HTML input attributes via spreading (`...rest`).

### PhoneValue Type

The `value` and `onChange` props work with a structured `PhoneValue` object:

```tsx
interface PhoneValue {
  full: string;      // Complete formatted number: "+33 12 34 56 78"
  prefix?: string;   // Country code (split mode only): "+33"
  number?: string;   // Formatted number (split mode only): "12 34 56 78"
  raw: string;       // Digits only, for backend: "33123456789"
}
```

### PhonePrefix Type

When using split mode with custom prefixes:

```tsx
interface PhonePrefix {
  code: string;       // e.g., "+33"
  label?: string;     // Short label shown in the closed selector, e.g., "Fra +33"
  fullLabel?: string; // Full label shown in the native dropdown list, e.g., "France +33"
  country?: string;   // e.g., "FR" (ISO country code)
}
```

## Notes

### Pattern Formatting

The component formats phone numbers in real-time as users type. The `pattern` prop uses `#` as a placeholder for digits, with other characters treated as literals:

- `"## ## ## ## ##"` → `12 34 56 78 90`
- `"+1 (###) ###-####"` → `+1 (555) 123-4567`
- `"(###) ###-####"` → `(555) 123-4567`

The component automatically:
- Extracts only digits from user input
- Applies the pattern format
- Maintains cursor position while typing
- Prevents input exceeding pattern length

### Mode Comparison

**Full Mode (`mode="full"`):**
- Single input field with pattern-based formatting
- Best for national phone numbers with consistent format
- Simpler UI for users in single country
- Example: `12 34 56 78 90`

**Split Mode (`mode="split"`):**
- Separate prefix selector and number input
- Best for international phone numbers
- The prefix selector shows a short label (e.g. `Fra +33`) when closed, and the full country name (e.g. `France +33`) inside the native dropdown list — see [Prefix Selector UX](#prefix-selector-ux) below
- Example: `+33` (selector) + `12 34 56 78` (input)

### Default Prefixes

The component includes 24 common country code prefixes by default:

- US/Canada (+1), UK (+44), France (+33), Germany (+49), Italy (+39)
- Spain (+34), China (+86), India (+91), Japan (+81), South Korea (+82)
- Russia (+7), Australia (+61), Brazil (+55), Mexico (+52), Netherlands (+31)
- Sweden (+46), Switzerland (+41), Belgium (+32), Poland (+48), Portugal (+351)
- Greece (+30), Norway (+47), Denmark (+45), Finland (+358)

Each default prefix ships with both a `label` (short, e.g. `Fra +33`) and a `fullLabel` (long, e.g. `France +33`). You can override these by providing a custom `prefixes` array.

### Prefix Selector UX

The split-mode prefix selector uses a two-layer approach to show different text depending on context, with no JavaScript DOM mutation required:

- **Closed state** — a styled `<div>` (the "fake select") displays the short `label` of the currently selected prefix alongside a chevron icon. It is `aria-hidden="true"` and non-interactive (`pointer-events-none`).
- **Open state** — a transparent native `<select>` is positioned `absolute inset-0` over the fake element. Its `<option>` elements use `fullLabel` (falling back to `label` then `code`), so the browser's native dropdown lists full country names.

This keeps the two concerns cleanly separated: the short label lives in the visible `<div>`, and the full labels live in the hidden `<select>`.

### Value Structure

The component returns a rich `PhoneValue` object with multiple formats:

- **`full`**: Complete formatted number for display
- **`raw`**: Digits only (no formatting) - use this for API submissions
- **`prefix`** (split mode): Selected country code
- **`number`** (split mode): Formatted number without prefix

This structure provides flexibility for different use cases while maintaining a clean API.

### Validation States

The component supports three validation states via the `validationStatus` prop:

- `"default"`: Normal state with light gray border
- `"error"`: Red border with cross icon, displays `errorMessage` if provided
- `"success"`: Green border with check icon

The status icons appear on the right side of the input (or on the number input in split mode).

### Form Control Integration

PhoneField wraps the input in a `FormControl` component that provides consistent styling for labels, descriptions, error messages, and validation states across all form fields.

### Accessibility

- The input uses `type="tel"` which triggers numeric keyboards on mobile devices
- The component automatically associates the label with the input using the `id` attribute
- In split mode, both the prefix selector and number input have appropriate `aria-label` attributes; the decorative fake-select `<div>` is marked `aria-hidden="true"` so screen readers interact only with the real `<select>`
- Disabled fields have appropriate visual feedback with reduced opacity
- Required fields are indicated with a red asterisk (unless `hideRequiredStar` is true)
- Status icons (check/cross) provide visual validation feedback

### Cursor Position Management

The component intelligently manages cursor position during formatting. When you type digits and the component adds spaces or special characters, the cursor stays at the correct position relative to the digits you entered, not jumping to the end of the input.

### Client Component

This component uses React hooks (`useState`, `useEffect`, `useRef`, `useId`) and must be used in a client component. Add `"use client"` at the top of files that use this component.

## Examples

### US Phone Number

```tsx
<PhoneField
  label="Phone Number"
  mode="full"
  pattern="+1 (###) ###-####"
  placeholder="Enter US phone number"
/>
```

### French Phone Number

```tsx
<PhoneField
  label="Numéro de téléphone"
  mode="full"
  pattern="+33 ## ## ## ## ##"
  placeholder="Entrez votre numéro"
/>
```

### International with Custom Prefixes

```tsx
<PhoneField
  label="Contact Number"
  mode="split"
  prefixes={[
    { code: '+1',  label: 'US +1',   fullLabel: 'United States +1',  country: 'US' },
    { code: '+33', label: 'Fra +33', fullLabel: 'France +33',         country: 'FR' },
    { code: '+44', label: 'UK +44',  fullLabel: 'United Kingdom +44', country: 'GB' },
  ]}
  defaultPrefix="+33"
  pattern="## ## ## ## ##"
/>
```

### With Validation

```tsx
<PhoneField
  label="Phone Number"
  mode="full"
  pattern="## ## ## ## ##"
  validationStatus="error"
  errorMessage="Please enter a valid phone number"
/>
```

### With Icon

```tsx
<PhoneField
  label="Phone Number"
  mode="full"
  pattern="## ## ## ## ##"
  icon="CalendarDefault"
/>
```
