Masked Input
Text input with auto-formatting masks for credit cards, phones, and dates.
Installation#
npx @aniui/cli add masked-inputWeb preview — components render natively on iOS & Android
import { MaskedInput } from "@/components/ui/masked-input";
export function MyScreen() {
return (
<View className="gap-4">
{/* Credit card preset */}
<MaskedInput
preset="credit-card"
placeholder="1234 5678 9012 3456"
onChangeText={(masked, raw) => console.log(masked, raw)}
/>
{/* Phone preset */}
<MaskedInput
preset="phone"
placeholder="(555) 123-4567"
onChangeText={(masked, raw) => console.log(masked, raw)}
/>
{/* Date preset */}
<MaskedInput
preset="date"
placeholder="MM/DD/YYYY"
onChangeText={(masked, raw) => console.log(masked, raw)}
/>
{/* Custom mask */}
<MaskedInput
mask="###-##-####"
placeholder="SSN"
onChangeText={(masked, raw) => console.log(masked, raw)}
/>
</View>
);
}Usage#
app/index.tsx
import { MaskedInput } from "@/components/ui/masked-input";
export function MyScreen() {
return (
<View className="gap-4">
{/* Credit card preset */}
<MaskedInput
preset="credit-card"
placeholder="1234 5678 9012 3456"
onChangeText={(masked, raw) => console.log(masked, raw)}
/>
{/* Phone preset */}
<MaskedInput
preset="phone"
placeholder="(555) 123-4567"
onChangeText={(masked, raw) => console.log(masked, raw)}
/>
{/* Date preset */}
<MaskedInput
preset="date"
placeholder="MM/DD/YYYY"
onChangeText={(masked, raw) => console.log(masked, raw)}
/>
{/* Custom mask */}
<MaskedInput
mask="###-##-####"
placeholder="SSN"
onChangeText={(masked, raw) => console.log(masked, raw)}
/>
</View>
);
}Props#
PropTypeDefault
variant"default" | "ghost""default"size"sm" | "md" | "lg""md"maskstring-preset"credit-card" | "phone" | "date"-onChangeText(masked: string, raw: string) => void-classNamestring-Also accepts all TextInput props except onChangeText.
Accessibility#
accessibilityRole="text"with auto-formatting for credit card, phone, and date masks.- Formatted value is announced to screen readers as the user types.
Source#
components/ui/masked-input.tsx
import React, { useCallback } from "react";
import { TextInput } from "react-native";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const maskedVariants = cva(
"rounded-md border py-2 text-foreground placeholder:text-muted-foreground",
{
variants: {
variant: {
default: "border-input bg-background",
ghost: "border-transparent bg-transparent",
},
size: {
sm: "min-h-9 px-3 text-sm",
md: "min-h-12 px-4 text-base",
lg: "min-h-14 px-5 text-lg",
},
},
defaultVariants: { variant: "default", size: "md" },
}
);
type MaskPreset = "credit-card" | "phone" | "date";
const masks: Record<MaskPreset, string> = {
"credit-card": "#### #### #### ####",
phone: "(###) ###-####",
date: "##/##/####",
};
function applyMask(raw: string, mask: string): string {
const digits = raw.replace(/\D/g, "");
let result = "";
let digitIdx = 0;
for (let i = 0; i < mask.length && digitIdx < digits.length; i++) {
if (mask[i] === "#") {
result += digits[digitIdx++];
} else {
result += mask[i];
}
}
return result;
}
export interface MaskedInputProps
extends Omit<React.ComponentPropsWithoutRef<typeof TextInput>, "onChangeText">,
VariantProps<typeof maskedVariants> {
className?: string;
mask?: string;
preset?: MaskPreset;
onChangeText?: (masked: string, raw: string) => void;
}
export function MaskedInput({
variant,
size,
className,
mask: customMask,
preset,
onChangeText,
...props
}: MaskedInputProps) {
const maskPattern = customMask ?? (preset ? masks[preset] : "");
const handleChange = useCallback(
(text: string) => {
if (!maskPattern) {
onChangeText?.(text, text);
return;
}
const raw = text.replace(/\D/g, "");
const masked = applyMask(raw, maskPattern);
onChangeText?.(masked, raw);
},
[maskPattern, onChangeText]
);
return (
<TextInput
className={cn(maskedVariants({ variant, size }), className)}
placeholderTextColor="hsl(240 3.8% 46.1%)"
keyboardType="number-pad"
onChangeText={handleChange}
{...props}
/>
);
}