Skip to main content

PhoneNumber

A highly customizable phone input component with automatic formatting and international country code support. Built on top of react-phone-input-2, the PhoneNumber component seamlessly integrates with RizzUI's design system, providing a consistent and accessible user experience.

Phone

Features

  • 🌍 International Support - Automatic country code detection and formatting
  • 🎨 Multiple Variants - Choose from outline, flat, or text styles
  • 📏 Flexible Sizing - Three size options (sm, md, lg) to match your design
  • Clearable Input - Optional clear button for better UX
  • 🔍 Searchable Dropdown - Easy country selection with search functionality
  • Accessible - Built with accessibility best practices
  • 🎯 Type Safe - Full TypeScript support

Installation


Step 1

Install the react-phone-input-2 package.

npm install react-phone-input-2 tailwind-variants

Step 2

Create phone number component, components/phone-number.tsx

import React from 'react';
import PhoneInput from 'react-phone-input-2';
import type { PhoneInputProps } from 'react-phone-input-2';
import 'react-phone-input-2/lib/style.css';
import { tv, type VariantProps } from 'tailwind-variants';
import { cn } from 'rizzui/cn';
import { FieldErrorText } from 'rizzui/field-error-text';
import { FieldHelperText } from 'rizzui/field-helper-text';
import { FieldClearButton } from 'rizzui/field-clear-button';

const labelStyles = {
size: {
sm: 'text-xs mb-1',
md: 'text-sm mb-1.5',
lg: 'text-sm mb-1.5',
},
} as const;

const phoneNumber = tv({
slots: {
input:
'block peer !w-full focus:outline-none transition duration-200 disabled:!bg-gray-100 disabled:!text-gray-500 disabled:placeholder:!text-gray-400 disabled:!cursor-not-allowed disabled:!border-gray-200 rounded-[var(--border-radius)]',
button:
'!border-0 !bg-transparent !static [&>.selected-flag]:!absolute [&>.selected-flag]:!top-[1px] [&>.selected-flag]:!bottom-[1px] [&>.selected-flag]:!left-[1px] [&>.selected-flag.open]:!bg-transparent [&>.selected-flag:hover]:!bg-transparent [&>.selected-flag:focus]:!bg-transparent',
dropdown:
'!border !border-border !shadow-xl !text-sm !max-h-[216px] !w-full !my-1.5 !bg-gray-50 [&>.no-entries-message]:!text-center [&>.divider]:!border-muted !rounded-[var(--border-radius)] [&>li.country.highlight]:!bg-primary-lighter/70 [&>li.country:hover]:!bg-primary-lighter/70 !p-0',
searchBox:
'!pr-2.5 !bg-gray-50 [&>.search-box]:!w-full [&>.search-box]:!m-0 [&>.search-box]:!px-3 [&>.search-box]:!py-1 [&>.search-box]:!text-sm [&>.search-box]:!capitalize [&>.search-box]:!h-9 [&>.search-box]:!leading-[36px] [&>.search-box]:!rounded-md [&>.search-box]:!bg-transparent [&>.search-box]:!border-muted [&>.search-box:focus]:!border-gray-400/70 [&>.search-box:focus]:!ring-0 [&>.search-box]:placeholder:!text-gray-500',
label: '',
clearButton:
'absolute right-2 group-hover/phone-number:visible group-hover/phone-number:translate-x-0 group-hover/phone-number:opacity-100',
},
variants: {
variant: {
flat: {
input:
'!border-0 focus:ring-2 placeholder:!opacity-90 read-only:focus:!ring-0 !bg-primary-lighter/70 hover:enabled:!bg-primary-lighter/90 focus:!ring-primary/30 !text-primary-dark',
},
outline: {
input:
'!bg-transparent focus:ring-[0.6px] !border !border-muted read-only:!border-muted read-only:focus:!ring-0 placeholder:!text-gray-500 hover:!border-primary focus:!border-primary focus:!ring-primary',
},
text: {
input:
'!border-0 focus:ring-2 !bg-transparent hover:!text-primary-dark focus:!ring-primary/30 !text-primary',
},
},
size: {
sm: {
input: 'py-1 !text-xs !h-8 !leading-[32px]',
button: '[&>.selected-flag]:!h-[30px]',
clearButton: 'top-[9px]',
label: labelStyles.size.sm,
},
md: {
input: 'py-2 !text-sm !h-10 !leading-[40px]',
button: '[&>.selected-flag]:!h-[38px]',
clearButton: 'top-3',
label: labelStyles.size.md,
},
lg: {
input: 'py-2 !text-base !h-12 !leading-[48px]',
button: '[&>.selected-flag]:!h-[46px]',
clearButton: 'top-4',
label: labelStyles.size.lg,
},
},
error: {
true: {
input:
'!border-red hover:enabled:!border-red focus:enabled:!border-red focus:!ring-red',
},
},
disabled: {
true: {
button: 'pointer-events-none',
},
},
readOnly: {
true: {
button: 'pointer-events-none',
},
},
},
defaultVariants: {
variant: 'outline',
size: 'md',
},
});

export interface PhoneNumberProps
extends Omit<
PhoneInputProps,
| 'inputClass'
| 'buttonClass'
| 'containerClass'
| 'dropdownClass'
| 'searchClass'
| 'enableSearch'
| 'disableSearchIcon'
> {
label?: React.ReactNode;
error?: string;
size?: VariantProps<typeof phoneNumber>['size'];
variant?: VariantProps<typeof phoneNumber>['variant'];
clearable?: boolean;
enableSearch?: boolean;
onClear?: (event: React.MouseEvent) => void;
labelClassName?: string;
inputClassName?: string;
buttonClassName?: string;
dropdownClassName?: string;
searchClassName?: string;
helperClassName?: string;
errorClassName?: string;
helperText?: React.ReactNode;
className?: string;
}

const PhoneNumber = ({
size = 'md',
variant = 'outline',
label,
helperText,
error,
clearable,
onClear,
enableSearch,
labelClassName,
inputClassName,
buttonClassName,
dropdownClassName,
searchClassName,
helperClassName,
errorClassName,
className,
...props
}: PhoneNumberProps) => {
const inputProps = (props.inputProps || {}) as {
disabled?: boolean;
readOnly?: boolean;
};

const {
input: inputClass,
button: buttonClass,
dropdown: dropdownClass,
searchBox: searchBoxClass,
label: labelClass,
clearButton: clearButtonClass,
} = phoneNumber({
size,
variant,
error: Boolean(error),
disabled: Boolean(inputProps.disabled),
readOnly: Boolean(inputProps.readOnly),
});

return (
<div className={cn('rizzui-phone-number', className)}>
{label && (
<label
className={cn('block font-medium', labelClass(), labelClassName)}
>
{label}
</label>
)}
<div className="relative group/phone-number">
<PhoneInput
inputClass={cn(inputClass(), inputClassName)}
buttonClass={cn(buttonClass(), buttonClassName)}
dropdownClass={cn(dropdownClass(), dropdownClassName)}
searchClass={cn(searchBoxClass(), searchClassName)}
enableSearch={enableSearch}
disableSearchIcon
{...props}
/>
{clearable && (
<FieldClearButton
size={size}
onClick={onClear}
className={clearButtonClass()}
/>
)}
</div>
{!error && helperText && (
<FieldHelperText size={size} className={helperClassName}>
{helperText}
</FieldHelperText>
)}
{error && (
<FieldErrorText size={size} error={error} className={errorClassName} />
)}
</div>
);
};

PhoneNumber.displayName = 'PhoneNumber';
export default PhoneNumber;

Usage


Basic Example

The simplest way to use the PhoneNumber component with default settings:

Phone
import PhoneNumber from '@components/phone-number';

export default function App() {
return (
<PhoneNumber
label="Phone Number"
country="us"
preferredCountries={['us']}
/>
);
}

Controlled Component

For controlled usage, manage the phone number value with state:

Phone
import React from 'react';
import PhoneNumber from '@components/phone-number';

export default function App() {
const [phoneNumber, setPhoneNumber] = React.useState('');

return (
<PhoneNumber
value={phoneNumber}
country="us"
label="Phone Number"
preferredCountries={['us']}
onChange={(value: string) => setPhoneNumber(value)}
/>
);
}

Variants

The PhoneNumber component supports three visual variants to match your design needs:

Phone
Phone
Phone
import PhoneNumber from '@components/phone-number';

export default function App() {
return (
<>
<PhoneNumber label="Outline Variant" country="us" variant="outline" />
<PhoneNumber label="Flat Variant" country="us" variant="flat" />
<PhoneNumber label="Text Variant" country="us" variant="text" />
</>
);
}

Sizes

Choose from three size options to match your form's density and design requirements:

Phone
Phone
Phone
import PhoneNumber from '@components/phone-number';

export default function App() {
return (
<>
<PhoneNumber label="Small Size" country="us" size="sm" />
<PhoneNumber label="Medium Size (Default)" country="us" size="md" />
<PhoneNumber label="Large Size" country="us" size="lg" />
</>
);
}

Clearable Input

Enable the clear button to allow users to quickly reset the phone number field. The clear button appears when the input has a value:

Phone
import React from 'react';
import PhoneNumber from '@components/phone-number';

export default function App() {
const [phoneNumber, setPhoneNumber] = React.useState('');

return (
<PhoneNumber
value={phoneNumber}
country="us"
label="Phone Number"
preferredCountries={['us']}
onChange={(value: string) => setPhoneNumber(value)}
clearable={!!phoneNumber}
onClear={() => {
setPhoneNumber('');
}}
/>
);
}

Searchable Dropdown

Enable search functionality in the country dropdown for easier country selection, especially useful when dealing with many countries:

Phone
import PhoneNumber from '@components/phone-number';

export default function App() {
return <PhoneNumber label="Phone Number" country="us" enableSearch />;
}

Disabled State

Disable the phone number input to prevent user interaction:

Phone
import PhoneNumber from '@components/phone-number';

export default function App() {
return <PhoneNumber label="Phone Number" country="us" disabled />;
}

Helper Text

Provide additional context or instructions using the helperText prop:

Phone
Include country code. We'll never share your number.
import PhoneNumber from '@components/phone-number';

export default function App() {
return (
<PhoneNumber
label="Phone Number"
country="us"
helperText="Include country code. We'll never share your number."
/>
);
}

Error State

Display validation errors using the error prop. When an error is present, the helper text is automatically hidden:

Phone
import PhoneNumber from '@components/phone-number';

export default function App() {
return (
<PhoneNumber
label="Phone Number"
country="us"
error="Please enter a valid phone number."
/>
);
}

Advanced Usage

Custom Styling

You can customize individual parts of the component using className props:

import PhoneNumber from '@components/phone-number';

export default function App() {
return (
<PhoneNumber
label="Phone Number"
country="us"
inputClassName="custom-input-styles"
buttonClassName="custom-button-styles"
dropdownClassName="custom-dropdown-styles"
labelClassName="custom-label-styles"
/>
);
}

Preferred Countries

Set preferred countries that appear at the top of the dropdown:

import PhoneNumber from '@components/phone-number';

export default function App() {
return (
<PhoneNumber
label="Phone Number"
country="us"
preferredCountries={['us', 'ca', 'gb']}
/>
);
}

API Reference


PhoneNumber Props

PropTypeDescriptionDefault
labelReactNodeThe label text displayed above the inputundefined
errorstringError message to display below the input. When provided, helper text is hiddenundefined
variant"outline" | "flat" | "text"Visual style variant of the component"outline"
size"sm" | "md" | "lg"Size of the component. "sm" provides dense styling"md"
clearablebooleanShow a clear button when input has a valuefalse
enableSearchbooleanEnable search functionality in the country dropdownfalse
onClear(event: React.MouseEvent) => voidCallback fired when the clear button is clickedundefined
helperTextReactNodeHelper text displayed below the input (hidden when error is present)undefined
labelClassNamestringAdditional CSS classes for the label elementundefined
inputClassNamestringAdditional CSS classes for the input elementundefined
buttonClassNamestringAdditional CSS classes for the country selector buttonundefined
dropdownClassNamestringAdditional CSS classes for the country dropdownundefined
searchClassNamestringAdditional CSS classes for the search input in dropdownundefined
helperClassNamestringAdditional CSS classes for the helper textundefined
errorClassNamestringAdditional CSS classes for the error messageundefined
classNamestringAdditional CSS classes for the root wrapper elementundefined

Note: The PhoneNumber component extends all props from react-phone-input-2, allowing you to use features like country, preferredCountries, onChange, value, inputProps, and more. Refer to the react-phone-input-2 documentation for a complete list of available props.

PhoneNumber Variants

type PhoneNumberVariants = 'outline' | 'flat' | 'text';
  • outline - Input with border and transparent background (default)
  • flat - Input with colored background and no border
  • text - Minimal input with no border or background

PhoneNumber Sizes

type PhoneNumberSizes = 'sm' | 'md' | 'lg';
  • sm - Small size with compact padding (32px height)
  • md - Medium size with standard padding (40px height) - default
  • lg - Large size with generous padding (48px height)

Best Practices

  • Always provide a label - Labels improve accessibility and user experience
  • Use helper text - Provide context about the expected format or purpose
  • Validate input - Use the error prop to display validation feedback
  • Set preferred countries - Improve UX by showing commonly used countries first
  • Enable search - Consider enabling search when your app targets international users
  • Handle onChange - Use controlled components for form integration