Skip to content

Repository files navigation

Maskit

A modular, framework-agnostic input masking library for JavaScript and TypeScript.

Maskit provides a headless core engine for parsing, validating, and formatting masked input — plus optional packages for DOM integration, framework bindings, and pre-built mask aliases.

Features

  • Headless core — zero DOM dependency, runs anywhere (Node.js, Deno, Bun, browsers)
  • Rich mask syntax — optional sections [...], alternations (a|b), quantifiers {min,max}, escape characters, regex masks
  • Unicode-aware — built-in definitions use \p{N} (digits) and \p{L} (letters) for international input
  • Extensible — register custom definitions, aliases, and validators
  • Framework bindings — SolidJS directive/component, Web Component, vanilla DOM
  • Pre-built aliases — date/time, numeric/currency, IP, email, SSN, MAC, VIN, URL, and more
  • Tree-shakeable — ES module builds with explicit register*() opt-in functions
  • Type-safe — written in TypeScript with full type declarations

Packages

PackageDescriptionDepends On
@magik_io/maskit-coreHeadless mask engine, parser, validator
@magik_io/maskit-domDOM integration (event binding, caret, value patching)@magik_io/maskit-core
@maskit/dateDate/time mask aliases@magik_io/maskit-core
@maskit/numericNumeric, currency, percentage, integer aliases@magik_io/maskit-core
@maskit/extensionsIP, email, MAC, VIN, SSN, URL, CSS unit aliases@magik_io/maskit-core
@maskit/solidSolidJS directive and <MaskedInput> component@magik_io/maskit-core, @magik_io/maskit-dom
@maskit/web-component<input-mask> custom element (form-associated)@magik_io/maskit-core, @magik_io/maskit-dom

Quick Start

Headless (Node.js / any runtime)

import{createMask,format,unformat,isValid}from"@magik_io/maskit-core";// One-shot formattingformat("1234567890",{mask: "(999) 999-9999"});// → "(123) 456-7890"// One-shot unformattingunformat("(123) 456-7890",{mask: "(999) 999-9999"});// → "1234567890"// One-shot validationisValid("(123) 456-7890",{mask: "(999) 999-9999"});// → true// Stateful engineconstengine=createMask({mask: "99/99/9999"});engine.processInput("1");engine.processInput("2");engine.processInput("2");engine.processInput("5");engine.processInput("2");engine.processInput("0");engine.processInput("2");engine.processInput("5");engine.getValue();// → "12/25/2025"engine.getUnmaskedValue();// → "12252025"engine.isComplete();// → true

DOM (Vanilla JavaScript)

import{mask,unmask}from"@magik_io/maskit-dom";constinput=document.querySelector("#phone");constcontroller=mask(input,{mask: "(999) 999-9999"});// Read valuescontroller.value();// → "(123) 456-7890"controller.unmaskedValue();// → "1234567890"// Set values programmaticallycontroller.setValue("9876543210");// Clean upcontroller.destroy();

DOM (Auto-init via data attributes)

<inputdata-maskit="(999) 999-9999" data-maskit-placeholder="_" /><scripttype="module">import{autoInit}from"@magik_io/maskit-dom";autoInit();</script>

SolidJS

import{maskit,MaskedInput}from"@maskit/solid";// DirectivefunctionApp(){// Required to prevent tree-shakingvoidmaskit;return<inputuse:maskit={{mask: "(999) 999-9999"}}/>;}// ComponentfunctionApp(){return(<MaskedInputoptions={{mask: "(999) 999-9999"}}onController={(ctrl)=>console.log(ctrl.value())}/>);}

Web Component

<scripttype="module">import{register}from"@maskit/web-component";register();// registers <input-mask></script><input-maskmask="(999) 999-9999"></input-mask>

Mask Syntax

Definitions (built-in)

CharacterMatchesDescription
9\p{N}Any Unicode digit
a\p{L}Any Unicode letter
*[\p{L}\p{N}]Any letter or digit

Any character not in the definitions table is treated as a static literal.

Syntax Elements

SyntaxDescriptionExample
[...]Optional section99[99] — 2 or 4 digits
(a|b)Alternation group(999) 999-9999|(999)999-9999
{min,max}Quantifiera{2,4} — 2 to 4 letters
{n}Exact quantifier9{4} — exactly 4 digits
{+}One or more9{+} — 1+ digits
{*}Zero or more9{*} — 0+ digits
(...)Group(999){3} — three groups of 3 digits
\Escape next character\9 — literal "9"

Regex Masks

Pass a regex string to use regex-based masking:

createMask({regex: "[0-9]{1,3}(\\.[0-9]{1,3}){3}"});// IPv4

Using Aliases

Aliases are pre-configured mask option sets. Register them explicitly, then reference by name:

Date/Time

import{registerDate}from"@maskit/date";import{createMask}from"@magik_io/maskit-core";registerDate();// registers "datetime" aliasconstengine=createMask({alias: "datetime",inputFormat: "MM/dd/yyyy",});

Date format tokens:d, dd, M, MM, MMM, MMMM, yy, yyyy, h, hh, H, HH, m, mm, s, ss, l, L, t, tt, T, TT, Z

Built-in date formats:isoDate (yyyy-MM-dd), isoTime (HH:mm:ss), isoDateTime (yyyy-MM-dd\THH:mm:ss)

Numeric

import{registerNumeric}from"@maskit/numeric";import{createMask}from"@magik_io/maskit-core";registerNumeric();// registers numeric, currency, decimal, integer, percentage, indiannsconstcurrency=createMask({alias: "currency"});// groupSeparator: ",", digits: 2, radixPoint: "."constpercentage=createMask({alias: "percentage"});// min: 0, max: 100, suffix: " %", digits: 0
AliasDescription
numericGeneral numeric input (configurable digits, grouping, negation)
currencyNumeric with , separators, 2 fixed decimals
decimalSame as numeric (no overrides)
integerNumeric with digits: 0 — no decimal part
percentage0–100 with % suffix
indiannsIndian numbering system grouping

Extensions

import{registerExtensions}from"@maskit/extensions";import{createMask}from"@magik_io/maskit-core";registerExtensions();// registers ip, email, mac, vin, ssn, url, cssunit + A, &, # defsconstemail=createMask({alias: "email"});constip=createMask({alias: "ip"});constssn=createMask({alias: "ssn"});
AliasMask PatternDescription
ipi{1,3}.j{1,3}.k{1,3}.l{1,3}IPv4 address (0–255 per octet)
email*{1,64}@-{1,63}.-{1,63}[...]Email address
mac##:##:##:##:##:##MAC address (hex)
vinV{13}9{4}Vehicle Identification Number
ssn999-99-9999US Social Security Number (with validation)
url(https?|ftp)://.*URL (regex)
cssunitRegex for CSS values10px, 1.5em, 100%, etc.

Extension definitions:

CharacterMatchesCasing
ALetters (incl. Cyrillic, Latin Extended)upper
&Letters + digits (incl. Cyrillic, Latin Extended)upper
#Hex digits (0-9A-Fa-f)upper

Configuration Options

Key options available via createMask() and mask():

OptionTypeDefaultDescription
maskstring | string[] | FunctionnullThe mask pattern
regexstringnullRegex-based mask (alternative to mask)
aliasstringnullReference a registered alias
placeholderstring"_"Placeholder character for unfilled positions
greedybooleanfalseWhether the mask buffer grows to accommodate optional/quantifier content
repeatnumber0Repeat the mask pattern (0 = no repeat)
insertModebooleantrueInsert vs. overwrite mode
showMaskOnFocusbooleantrueShow mask template on focus (DOM)
showMaskOnHoverbooleantrueShow mask template on hover (DOM)
clearMaskOnLostFocusbooleantrueClear template text on blur if empty (DOM)
clearIncompletebooleanfalseClear value on blur if mask is incomplete
autoUnmaskbooleanfalseReturn unmasked value from .value getter
removeMaskOnSubmitbooleanfalseRemove mask from value on form submit
numericInputbooleanfalseRTL digit entry (right-to-left filling)
rightAlignbooleanfalseRight-align the input
casingstring | nullnull"upper", "lower", or "title"
keepStaticboolean | nullnullKeep static parts when switching alternations
jitMaskingboolean | numberfalseJust-in-time masking: defer static chars
nullablebooleantrueAllow empty value (return "" instead of mask template)
definitionsRecord<string, MaskDefinition>Custom mask character definitions
aliasesRecord<string, AliasDefinition>Custom alias definitions

Hooks (DOM)

HookSignatureDescription
onBeforeMask(value, opts) → stringTransform initial value before masking
onBeforePaste(pastedValue, opts) → stringTransform pasted value
onBeforeWrite(event, buffer, caretPos, opts) → WriteResultIntercept before writing to DOM
onUnMask(maskedValue, unmasked, opts) → stringTransform unmasked output
preValidation(buffer, pos, char, isSelection, opts, maskset, caretPos, strict) → boolean | CommandObjectPre-validation hook
postValidation(buffer, pos, char, currentResult, opts, maskset, strict, fromCheckval) → boolean | CommandObjectPost-validation hook
isComplete(buffer, opts, maskset) → booleanCustom completeness check
oncomplete() → voidFired when mask is complete
onincomplete() → voidFired when mask is incomplete on blur
oncleared() → voidFired when mask is cleared

Custom Definitions

import{createMask,defineDefinition}from"@magik_io/maskit-core";// Register globallydefineDefinition("H",{validator: "[0-9A-Fa-f]",casing: "upper",});// Or pass per-instanceconstengine=createMask({mask: "HH:HH:HH",definitions: {H: {validator: "[0-9A-Fa-f]",casing: "upper"},},});

MaskDefinition Properties

PropertyTypeDescription
validatorstring | RegExp | ValidatorFnRegex pattern or function to validate input
casing"upper" | "lower" | "title"Auto-case transformation
definitionSymbolstringSymbol to use in test resolution
staticbooleanWhether this position is static (non-editable)
optionalbooleanWhether this position is optional
placeholderstringCustom placeholder for this definition
generatedbooleanWhether this definition was auto-generated

Custom Aliases

import{defineAlias}from"@magik_io/maskit-core";defineAlias("phone-us",{mask: "(999) 999-9999",placeholder: "_",clearIncomplete: true,});// Use itconstengine=createMask({alias: "phone-us"});

Architecture

@magik_io/maskit-core Headless engine (no DOM)
├── mask-lexer Mask string → token AST
├── test-resolver Position → test match resolver
├── validation Character validation engine
└── engine Public API (createMask, format, etc.)
@magik_io/maskit-dom DOM integration layer
├── state WeakMap-based per-element state
├── caret Caret position management
├── value input.value get/set interception
├── event-handlers Keyboard, mouse, clipboard, form events
├── event-binding Event listener lifecycle
└── mask/auto-init mask() API and data-attribute scanning
@maskit/date Datetime alias
@maskit/numeric Numeric/currency aliases
@maskit/extensions IP, email, MAC, VIN, SSN, URL aliases
@maskit/solid SolidJS directive + component
@maskit/web-component <input-mask> custom element

Key Design Decisions

  • Headless-first: @magik_io/maskit-core has zero DOM dependency — it can run in any JS runtime
  • WeakMap state: DOM package stores all per-element state in WeakMaps — no property mutation on elements
  • Value interception: Object.defineProperty on the input instance intercepts .value get/set for transparent masking
  • Unicode validators: Built-in definitions use Unicode property escapes (\p{N}, \p{L}) for international input
  • No side effects on import: Registration functions (registerDate(), registerNumeric(), registerExtensions()) are explicit opt-ins
  • Function-preserving clone: Custom deepClone() handles RegExp and function validators that structuredClone cannot

Development

Prerequisites

  • Node.js ≥ 20
  • pnpm ≥ 9

Setup

pnpm install

Scripts

CommandDescription
pnpm buildBuild all packages
pnpm testRun all tests
pnpm test:watchRun tests in watch mode
pnpm test:coverageRun tests with coverage
pnpm lintLint all packages (Biome)
pnpm formatFormat all packages (Biome)
pnpm checkLint + format check (Biome)
pnpm check:writeLint + format with auto-fix (Biome)

Code Style

  • Formatter: Biome — 2-space indent, double quotes, trailing commas, semicolons always
  • Linting: Biome recommended rules + noExplicitAny: warn, noUnusedVariables: error
  • Testing: Vitest with v8 coverage
  • Build: Vite library mode → ESM + CJS dual output with TypeScript declarations

Releasing

This project uses Changesets for versioning:

pnpm changeset # Create a changeset
pnpm version # Apply changesets → bump versions
pnpm release # Build + publish to npm

License

MIT

About

A modern input mask library inspired by the inputmask library

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages