AniUI

Masked Input

Text input with auto-formatting masks for credit cards, phones, and dates.

Installation#

npx @aniui/cli add masked-input
Web 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"
mask
string
-
preset
"credit-card" | "phone" | "date"
-
onChangeText
(masked: string, raw: string) => void
-
className
string
-

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}
    />
  );
}