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.
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
- yarn
- pnpm
- bun
npm install react-phone-input-2 tailwind-variants
yarn add react-phone-input-2 tailwind-variants
pnpm add react-phone-input-2 tailwind-variants
bun add 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:
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:
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:
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:
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:
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:
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:
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:
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:
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
| Prop | Type | Description | Default |
|---|---|---|---|
| label | ReactNode | The label text displayed above the input | undefined |
| error | string | Error message to display below the input. When provided, helper text is hidden | undefined |
| variant | "outline" | "flat" | "text" | Visual style variant of the component | "outline" |
| size | "sm" | "md" | "lg" | Size of the component. "sm" provides dense styling | "md" |
| clearable | boolean | Show a clear button when input has a value | false |
| enableSearch | boolean | Enable search functionality in the country dropdown | false |
| onClear | (event: React.MouseEvent) => void | Callback fired when the clear button is clicked | undefined |
| helperText | ReactNode | Helper text displayed below the input (hidden when error is present) | undefined |
| labelClassName | string | Additional CSS classes for the label element | undefined |
| inputClassName | string | Additional CSS classes for the input element | undefined |
| buttonClassName | string | Additional CSS classes for the country selector button | undefined |
| dropdownClassName | string | Additional CSS classes for the country dropdown | undefined |
| searchClassName | string | Additional CSS classes for the search input in dropdown | undefined |
| helperClassName | string | Additional CSS classes for the helper text | undefined |
| errorClassName | string | Additional CSS classes for the error message | undefined |
| className | string | Additional CSS classes for the root wrapper element | undefined |
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
errorprop 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