Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Inputs

Masked Input

Inputs

Masked Input React component for Tailwind CSS v4

ListboxMention Input
Live props
Behavior
preset
mask
showMask
submitMasked
clearable
selectOnFocus
enterKeyHint
Content
maskPlaceholder
placeholderChar
placeholder
defaultValue
name
prefix
clearLabel
loadingLabel
Appearance
showValidity
showBrand
size
variant
radius
accent
floatingLabel
Field
label
description
error
required
invalid
disabled
readOnly
State
loading
Code
import { MaskedInput } from "@/components/ui/masked-input"

<MaskedInput />
  1. Home
  2. Components
  3. Inputs
  4. Masked Input
Pro

Masked Input React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React masked input that formats itself as you type or paste: card number, expiry, CVC, IBAN, CNP, postal code or a custom mask.

Type and the text shapes itself: spaces between card groups, a slash after the month, uppercase IBAN letters, and the caret always lands right after what you typed, skipping over the literals. Paste 4111-1111-1111-1111 or RO49 AAAA 1B31… with any separators and it is cleaned and re-masked; autofilled 12/2028 becomes 12/28. Backspace right after a space or slash deletes the digit before it, Delete the one after; selecting and typing replaces, and the browser's undo still works. In the expiry field a first digit from 2 to 9 is a month on its own (4 becomes 04/). With showMask the rest of the pattern shows as a faint ghost after the caret (on focus, or always). Card numbers show a card icon and the brand chip (Visa, Mastercard, Amex, Maestro, Discover) that pops in from the first digits. Once complete, a green check or a red alert icon pops in (Luhn, IBAN mod 97, CNP checksum and birth date, expiry not in the past); the red ring and aria-invalid wait until you leave the field. Enter submits the form. On a phone the keypad and autofill follow the preset (numeric for cards, CNP and postal codes, cc-number / cc-exp / cc-csc autofill), and the ghost keeps the format visible after the placeholder disappears.

Masked Input is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add masked-input, together with Input, Field, Motion presets, useControllableState and UIProvider and the npm packages motion and lucide-react.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add masked-input

Masked Input is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import MaskedInput from @/components/ui/masked-input.

import { useState } from "react"
import { MaskedInput, type MaskedInputDetails } from "@/components/ui/masked-input"

export function CardForm() {
  const [card, setCard] = useState("")
  const [details, setDetails] = useState<MaskedInputDetails | null>(null)
  const [touched, setTouched] = useState(false)

  return (
    <form className="flex flex-col gap-4">
      <MaskedInput
        preset="card-number"
        name="card"
        label="Card number"
        placeholder="1234 5678 9012 3456"
        value={card}
        onValueChange={(raw, info) => {
          setCard(raw)
          setDetails(info)
        }}
        onBlur={() => setTouched(true)}
        error={touched && details?.valid === false ? "The card number is not valid." : undefined}
        required
      />
      <div className="grid grid-cols-2 gap-3">
        <MaskedInput preset="card-expiry" name="expiry" label="Expiry" placeholder="MM/YY" showMask="focus" maskPlaceholder="MM/YY" />
        <MaskedInput preset="card-cvc" name="cvc" label="CVC" placeholder="123" enterKeyHint="done" />
      </div>
      <MaskedInput
        label="License plate (Romania)"
        mask={(raw) => (/^B\d/i.test(raw) ? "A 999 AAA" : "AA 99 AAA")}
        placeholder="CJ 12 ABC"
        autoCapitalize="characters"
      />
    </form>
  )
}

Props

  • presetdefault "none"
    "none" | "card-number" | "card-expiry" | "card-cvc" | "iban" | "cnp" | "postal-code"

    A ready mask with its validator, inputMode and autoComplete. card-number: 16 digits in groups of 4 (up to 19, Amex 4-6-5), Luhn, brand chip. card-expiry: 99/99, smart month, not in the past. card-cvc: 3 digits. iban: country length, uppercase, mod 97. cnp: 13 digits, checksum, birth date, county 01-52. postal-code: 6 digits. The playground shows the card form while none has no mask.

  • maskdefault ""
    string | ((raw: string) => string)

    The pattern: 9 a digit, a a letter, A a letter shown uppercase, * a letter or digit, \ escapes; anything else is a literal ("AA 99 AAA"). A function picks the pattern from the raw value typed so far (Amex cards, Bucharest plates). Overrides the preset's mask; the preset's validator stays.

  • showMaskdefault "never"
    "never" | "focus" | "always"

    The rest of the mask after the caret as a faint ghost (text-muted-foreground/60), aligned with the typed text. focus shows it while the field is focused; always keeps it visible and replaces the placeholder. A resting floating label hides it.

  • maskPlaceholderdefault ""
    string

    The ghost text for the whole mask, character by character ("mm/dd/yyyy" for 99/99/9999). Empty shows placeholderChar in each token.

  • placeholderChardefault "_"
    string

    The ghost character for each token when there is no maskPlaceholder.

  • showValiditydefault true
    boolean

    A trailing CircleCheck (success) or CircleAlert (danger) that pops in once the value is complete, with a visually hidden Valid / Invalid. Needs a validator (the preset's or validate).

  • showBranddefault true
    boolean

    card-number: the leading card icon and the trailing brand chip read from the first digits.

  • submitMaskeddefault false
    boolean

    The hidden form input (rendered when name is set) sends the masked text (4111 1111 …) instead of the raw digits. The readout under the demo shows what the form gets.

  • labeldefault ""
    ReactNode

    Renders the field anatomy around the box, linked with htmlFor. Leave it empty for a bare field (then pass aria-label).

  • descriptiondefault ""
    ReactNode

    Helper text under the box, linked with aria-describedby; a good place for the format ("Format: LL/AA").

  • errordefault ""
    ReactNode

    Replaces the description with the animated error and marks the field invalid. Say why the value failed (the demo does).

  • requireddefault false
    boolean

    The red mark after the label and the native required attribute.

  • invaliddefault false
    boolean

    The invalid look and aria-invalid without a message. A complete value that fails validation sets it by itself after the first blur.

  • disableddefault false
    boolean

    Dims the field and blocks input; the hidden form input is disabled too, so nothing is submitted. The demo fills in the test card.

  • readOnlydefault false
    boolean

    The value stays visible and selectable but cannot change. The demo fills in the test card.

  • placeholderdefault ""
    string

    Hint text while empty. Hidden while an always-visible ghost covers the field.

  • defaultValuedefault ""
    string

    The starting raw value while uncontrolled, with or without literals ("4111111111111111").

  • namedefault ""
    string

    Renders a hidden input with this name and the raw value (or the masked text with submitMasked), so native forms and FormData get the clean value.

  • sizedefault "md"
    "sm" | "md" | "lg"

    Height 32 / 40 / 48px with 13 / 14 / 15px text (16px on phones so iOS never zooms); md grows to 44px on touch.

  • variantdefault "soft"
    "soft" | "outline" | "underline" | "ghost"

    The Input's box look: a filled well, a bordered box, a bottom line or no fill until hover.

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg" | "full"

    Corner shape, scaled with the size like the button's; full makes a pill. Ignored by underline.

  • accentdefault ""
    string

    Any CSS color for the focus ring, the outline border on focus and the caret. Empty uses the ring token.

  • floatingLabeldefault false
    boolean

    The label rests inside the box and floats up on focus or when filled; the ghost waits until it has floated.

  • clearabledefault false
    boolean

    A clear button while there is a value; Escape clears from the keyboard.

  • prefixdefault ""
    ReactNode

    Text before the input in the muted addon look, like "RO" or "+40", or an element rendered as is. A click on it lands in the input.

  • clearLabeldefault "Clear"
    string

    The clear button's accessible name and title (with clearable).

  • loadingdefault false
    boolean

    A small spinner at the end and aria-busy, e.g. while the server checks an IBAN. The field stays editable.

  • loadingLabeldefault "Loading"
    string

    The spinner's visually hidden text.

  • selectOnFocusdefault false
    boolean

    Selects the whole value when the field takes focus, by click, tap or Tab, so typing replaces it.

  • enterKeyHintdefault "next"
    "enter" | "done" | "go" | "next" | "previous" | "search" | "send"

    The label of the phone keyboard's Enter key. next suits a field in a form; pass done on the last one.

  • value
    string

    The raw value, controlled, without literals. A value longer than the mask shows cut but is not changed until the next edit.

  • onValueChange
    (raw: string, details: { masked: string; complete: boolean; valid: boolean | null; brand?: CardBrand | null }) => void

    Fires on every edit with the raw value and the details: the masked text, whether every token is filled, the validator's verdict once complete (null before), and the card brand for card-number.

  • validate
    (raw: string) => boolean

    Runs once the value is complete; replaces the preset's validator. Without one (and without a preset) valid stays null and no icon shows. The mask engine and the checks are exported too (see Parts), for server code, initial state and read-only displays.

  • labels
    Partial<{ valid: string; invalid: string; brands: Partial<Record<CardBrand, string>> }>

    The visually hidden text of the validity icon ("Valid" / "Invalid") and the brand chip names.

  • onChange
    (event: ChangeEvent<HTMLInputElement>) => void

    The native change event, untouched. Its target value is the masked text; read the raw value from onValueChange.

  • leftIcon
    ReactNode

    A leading icon; card-number shows a card icon by default (with showBrand).

  • suffix
    ReactNode

    Text or an element after the brand chip and the validity icon.

  • rightIcon
    ReactNode

    A trailing icon after the brand chip and the validity icon; it crossfades with the spinner while loading.

  • clearIcon
    ReactNode

    Replaces the clear button's icon (a 16px X by default).

  • onClear
    () => void

    Called after the clear button or Escape emptied the field (with clearable). onValueChange fires too, with an empty raw value.

  • fieldProps
    Partial<FieldProps>

    Anything else for the Field wrapper: optional and optionalLabel, labelAction, orientation, labelWidth, hideLabel, requiredIndicator.

  • ref
    RefObject<HTMLInputElement | null> or (node) => void

    The native input. Every other Input prop and native prop lands on it: inputMode, autoComplete and autoCapitalize (defaults from the preset), onBlur, aria-describedby. With react-hook-form use Controller (value / onValueChange), not register, since the element holds the masked text.

  • classNames
    Partial<Record<InputSlot | "ghost" | "brand" | "validity", string>>

    Extra classes for the Input's parts plus the ghost, the brand chip and the validity icon.

Parts

  • applyMask

    function

    applyMask(raw, mask) formats raw text with a pattern or a mask function: applyMask("4111111111111111", "9999 9999 9999 9999") returns "4111 1111 1111 1111".

  • getMaskDetails

    function

    getMaskDetails(raw, { preset, mask, validate }) returns what onValueChange hands back (masked, complete, valid, brand) for any raw value, e.g. for initial state or a server check.

  • detectCardBrand

    function

    detectCardBrand(digits) reads the brand from the first digits: "visa", "mastercard", "amex", "maestro", "discover" or null.

  • isValidLuhn

    function

    isValidLuhn(digits) runs the Luhn checksum card numbers use; spaces and dashes are ignored.

  • isValidIban

    function

    isValidIban(iban) checks the format, the length for known countries and the mod 97 checksum; spaces and case are ignored.

  • isValidCnp

    function

    isValidCnp(cnp, now?) validates a Romanian personal numeric code: the birth date, the county code and the control digit.

  • isValidExpiry

    function

    isValidExpiry(expiry, now?) accepts "MMYY" or "MM/YY" and is true through the last day of that month.

  • IBAN_LENGTHS

    constant

    The IBAN length per country code that isValidIban enforces (RO 24, DE 22, GB 22, FR 27 and a few more); other countries only get the checksum.

  • MaskedInputDetails

    type

    The details onValueChange receives: { masked, complete, valid, brand? }; valid is null until the value is complete.

Examples

IBAN

Letters turn uppercase, groups of four appear as you type and the length follows the country (22 for GB). A paste with spaces works; the second field fails the mod 97 check.

CNP (Romanian personal ID)

Checks the control digit, the birth date and the county code. The mask shows on focus; leave the field to see Invalid CNP.

Smart expiry

A first digit from 2 to 9 is a month on its own, so 4 becomes 04/ and the caret jumps past the slash. Past months are invalid.

Function mask

A Romanian plate: the mask is picked from what was typed, AA 99 AAA for counties, A 999 AAA once it starts with B and a digit (Bucharest).

Always-visible mask

showMask="always" with maskPlaceholder="mm/dd/yyyy": the format stays visible and the typed digits cover it one by one.

Raw or masked in a form

With name, a hidden input carries the raw digits; submitMasked sends the formatted text instead. Submit to print the FormData.

Dependencies

motionlucide-reactinputfieldmotionuse-controllable-stateui-config

Files

  • components/ui/masked-input.tsx

FAQ

Is the React Masked Input component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Masked Input?

Run npx wingo-ui@latest add masked-input. It also adds the Wingo UI items it builds on (Input, Field, Motion presets, useControllableState and UIProvider) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/masked-input.

How does Masked Input behave on phones?

On a phone the keypad and autofill follow the preset (numeric for cards, CNP and postal codes, cc-number / cc-exp / cc-csc autofill), and the ghost keeps the format visible after the placeholder disappears.

See also

InputFieldOTP InputNumber InputPassword Input

More React input components

Segmented ControlDate Range PickerCheckboxComboboxCountry SelectCron Picker
All 40 React input components

Articles using this component

  • IBAN Validation JavaScript: Mod 97, Regex and a React Field

    Component guides · Oct 9, 2026 · 12 min read

  • Romanian CNP Validator in TypeScript, Zod and React

    Component guides · Oct 9, 2026 · 13 min read

PreviousListboxNext Mention Input

Pay by card

Studio yearly plan

$49
Card
•••• •••• •••• ••••
Cardholder
Your name
Expires
MM/YY

Test card: 4111 1111 1111 1111

raw
–
form (raw)
–
valid
–