Sooner or later, every form needs a masked input: a phone number, a CPF, a currency field, a credit card. The instinct is to reach for npm and install something battle-tested. The problem is that "battle-tested" often means "battle-sized" — a masking library pulling in a parser, a runtime, and a handful of transitive dependencies just to reformat a string as the user types.
The actual problem is small
Input masking, at its core, is a straightforward transformation: take the raw characters a user types, walk them against a pattern, and reinsert literals (parentheses, dashes, slashes) at the right spots. There's no need for a state machine framework or a virtual DOM diffing engine. It's string manipulation triggered by a DOM event.
Yet a lot of the popular options treat it as a much bigger problem than it is. Some are written for a specific framework, others ship internationalization tables you'll never touch, others are frozen at whatever @types/* package the community last bothered to update. Every one of those is a dependency you now have to audit, patch, and carry through every npm audit for the life of the project — for a feature that's fundamentally a replace loop.
What a minimal solution actually needs
A masking utility only needs to do a few things well:
- Accept a pattern like
999.999.999-99or(99) 9-9999-9999, where tokens mean digit, letter, or alphanumeric. - Apply it to a string, pure — no DOM required, so it's testable and usable on the server too.
- Bind to an
<input>and re-apply on typing, without fighting the browser's own event loop. - Handle the edge cases that actually come up in real forms: switching from CPF to CNPJ mid-typing, applying the mask on blur instead of on every keystroke, reading the pattern from a
data-maskattribute so designers can change it without touching JS.
That's the whole surface area. Nothing about it requires a bundle measured in tens of kilobytes.
Zero dependencies, real TypeScript support
This is exactly the gap @tadashi/mask fills. It's a single-file, dependency-free ESM module — about 1 kB minified and gzipped — that does the masking transformation as a pure static function and, optionally, binds itself to an input element:
import Mask from '@tadashi/mask'
const mask = new Mask(input, { mask: 'SSS-9999', init: true })
Need the raw transformation without touching the DOM at all — say, to format a value before saving it, or in a Node.js script?
import Mask from '@tadashi/mask'
Mask.core('ABC12', 'SSS-9999') // => 'ABC-12'
TypeScript users aren't an afterthought either: types are generated straight from JSDoc annotations and shipped with the package, so tsc and your editor get full autocomplete and type-checking with no separate @types package to install, version, or fall out of sync.
You can see it in action, live, in this CodePen example — phone, CPF/CNPJ and other masks applied in real time as you type.
The takeaway
Before installing a masking library, it's worth asking what the problem actually requires. Most of the time it's pattern matching over a string, plus a thin event listener — not a framework. Reaching for a zero-dependency, purpose-built tool like @tadashi/mask keeps that promise: the bundle stays tiny, the dependency tree stays empty, and TypeScript support comes built in rather than bolted on.
Top comments (0)