ViraStack Mask: Input Masks for React Hook Form
In a checkout form, the card number field alone is enough trouble: how many digits, where the spaces go, whether the number is valid. Validity needs the Luhn algorithm; American Express changes the mask and grows the CVV to four digits. Phone numbers and IBANs bring rules of their own. Rewriting all of it in every project costs time and invites bugs.
ViraStack Mask (@virastack/mask) gives you ready, tested masks for these fields. It works with React Hook Form: the user sees the formatted value, the form keeps the raw one.
What it does
- Built for React Hook Form: The input shows
4111 1111 1111 1111, the form holds4111111111111111.reset,setValue,watchand resolvers keep working. - Type-safe: Field names are checked against your form type; a wrong one fails at compile time.
- International presets: E.164 phone numbers, any IBAN with mod-97, cards with Luhn, currencies through
Intl, dates. - Same rules on the server:
@virastack/mask/servervalidates the same schema in a Server Action. - Zero dependencies: Under 10 KB for everything.
Try it
The card number, expiry and CVV work together. Type an Amex number starting with 34 and watch the mask and the CVV length change. Under each field you'll see the raw value next to the formatted one.
Install
npm install @virastack/maskUsage
Pass the form and a schema; each key is a field name, each value a preset:
import { useForm } from "react-hook-form"
import { useViraMask } from "@virastack/mask"
type Card = { card: string; expiry: string; cvv: string }
export function CreditCardForm() {
const form = useForm<Card>({ defaultValues: { card: "", expiry: "", cvv: "" } })
const { card, expiry, cvv } = useViraMask(form, {
card: "card",
expiry: "expiry",
cvv: { preset: "cvv", from: "card" }
})
return (
<form onSubmit={form.handleSubmit(console.log)}>
<input {...card} placeholder="0000 0000 0000 0000" />
<input {...expiry} placeholder="MM/YY" />
<input {...cvv} placeholder="123" />
</form>
)
}Type 4111 1111 1111 1111 and form.watch("card") returns "4111111111111111", no spaces. from: "card" links the CVV to the card, so an Amex number switches it to four digits.
Validation errors come back as keys, not text, so the wording stays yours:
import { createMask } from "@virastack/mask/server"
createMask("card").validate("4111") // "card.incomplete"
createMask("card").validate("4111111111111112") // "card.invalid" (Luhn)
createMask("expiry").validate("0120") // "expiry.min", expired
createMask("iban").validate("GB82WEST12345698765431") // "iban.invalid" (mod-97)Add "phone", "iban", "date" or { preset: "currency", currency: "USD" } to the same schema for the other fields.
Every preset, server validation, resolvers and live examples are in the virastack.com/mask docs, and the source is in github.com/virastack/form. For a password field in the same form, see ViraStack Password.
