# numkey
Open source by [DevsLab](https://devslab.kr/brand/open-source/) [](https://www.npmjs.com/package/@devslab/numkey) [](https://github.com/devslab-kr/numkey/actions/workflows/ci.yml)  [](./LICENSE) [](https://stackblitz.com/github/devslab-kr/numkey/tree/main/examples/vanilla) **English** | [νκ΅μ΄](README.ko.md) Β· [πΊ Live demo](https://devslab-kr.github.io/numkey/) Try it online: [β‘ StackBlitz β Vanilla](https://stackblitz.com/github/devslab-kr/numkey/tree/main/examples/vanilla) Β· [Vue](https://stackblitz.com/github/devslab-kr/numkey/tree/main/examples/vue) Β· [React](https://stackblitz.com/github/devslab-kr/numkey/tree/main/examples/react) Β· [Svelte](https://stackblitz.com/github/devslab-kr/numkey/tree/main/examples/svelte) Β· [Solid](https://stackblitz.com/github/devslab-kr/numkey/tree/main/examples/solid) | [π¦ CodeSandbox](https://codesandbox.io/s/github/devslab-kr/numkey/tree/main/examples/vanilla) **"It's a string, but it's a number."** Every business app has these fields β amounts, quantities, prices β and every team rebuilds the same input by hand: live thousands grouping (`10,000`), no leading zeros, right-aligned, numeric keypad on mobile, and a caret that doesn't jump while separators appear and disappear around it. numkey is that input, done once: - **Caret-safe live formatting** β type into the middle of `1,234,567` and the cursor stays where you expect, even as groups reflow - **String-first value model** β the canonical value is a plain numeric string (`"1234567.89"`), never an IEEE 754 float, so money is safe - **Leading-zero cleanup** (`007` β `7`), full-width digit normalization (`οΌοΌοΌ` β `123` β Korean/Japanese IME), paste sanitizing (`β© 1,234μ` β `1234`) - **Right alignment + `inputmode`** set automatically (opt-out available) - **IME-safe** β nothing runs mid-composition - **Zero dependencies**, TypeScript-first, ESM/CJS dual + a CDN global build - Works with **plain ` ``` Server-rendered values (``) are formatted on load. To read the raw value back before submitting: ```html ``` (Or simply strip separators server-side β the posted value is the display value.) ### How the attributes work **`data-numkey` is the on-switch.** It is what gets an input bound (auto-init watches `input[data-numkey]`), and its value doubles as the max decimal places β empty means integers only. Every other `data-numkey-*` attribute is an *option* that is only read from inputs that have `data-numkey`; on its own it does nothing: ```html ``` | Attribute | Meaning | |---|---| | `data-numkey` | **the switch** β binds the input; the value is the max decimal places (empty = integer) | | `data-numkey-negative` | allow a leading minus | | `data-numkey-align="left"` | opt out of automatic right alignment | | `data-numkey-group="4"` | group size (default 3); `"3,2"` for Indian lakh β see below | | `data-numkey-separator=" "` | group separator (default `,`) | | `data-numkey-point=","` | decimal mark shown in the field (default `.`) | | `data-numkey-locale` | derive separators from a locale β see below | | `data-numkey-korean` | live Korean amount reading ("150λ§") β see below | | `data-numkey-korean-entry` | accept Korean shorthand entry ("3λ§5μ²" β blur β "35,000") β see below | | `data-numkey-name="amount"` | hidden input posting the canonical value β see below | | `data-numkey-min` / `data-numkey-max` | bounds, applied on blur only (typing is never interrupted) | ### Locale-aware display (opt-in) By default the display is **deterministic**: every visitor sees `1,234,567.89`, whatever their browser is set to β which is what business forms usually need. `data-numkey-locale` opts a field into locale separators: ```html ``` The locale changes **only how the value is drawn**. The canonical value is always `"1234567.89"` β `numkey.getValue(el)` returns the same string in all three cases. Since a plain form POST submits the *display* value, a form using locales should post through `data-numkey-name` (below) or normalize server-side. ### Indian lakh/crore grouping The Indian system groups the last three digits and then every two β `12,34,56,789`, not `123,456,789`. Give `group` a `[primary, secondary]` pair, or just name the locale: ```html ``` ```ts format('10000000', { group: [3, 2] }) // '1,00,00,000' (1 crore) format('123456789', { locale: 'en-IN' }) // '12,34,56,789' ``` An explicit `group` always wins over the locale, and a single number stays uniform grouping (`4` β `1234,5678` for λ§-style). `group: 0` turns separators off entirely. ### Korean amount reading (νκΈ κΈμ‘ λ³κΈ°) The "150λ§" hint that banking and fintech UIs render next to amount fields β usually rebuilt by hand in every project: ```html ``` An empty attribute generates a `` right after the input (unstyled β add "μ" with CSS `.numkey-korean::after { content: " μ" }`); a value is treated as a CSS selector for an existing element. The same engine is available as a pure function: ```ts import { toKorean } from '@devslab/numkey' toKorean('1500000') // "150λ§" toKorean('927483041001') // "9,274μ΅ 8,304λ§ 1,001" toKorean('100000001') // "1μ΅ 1" β zero groups omitted ``` And the inverse β accept the shorthand people actually type into λΆλμ°/μ£Όμ apps, either as a pure function or on the field itself: ```ts import { fromKorean } from '@devslab/numkey' fromKorean('3λ§5μ²') // "35000" fromKorean('1.5μ΅') // "150000000" fromKorean('μΌμμ€λ§') // "350000" ``` ```html ``` `getValue` and the `data-numkey-name` hidden sync (below) see the parsed value even while the draft is still on screen. ### Posting the canonical value (`data-numkey-name`) A plain form POST submits what the field displays β `1,234,567` β which every server handler then has to de-comma. `data-numkey-name` generates a hidden input that always carries the settled canonical value instead: ```html ``` The hidden value is kept settled even mid-typing (`1,234.` posts `1234`), so whatever moment the form submits, the server sees a clean number. > Use `type="text"` inputs. numkey sets `inputmode` so mobile keyboards show > the numeric keypad; `type="number"` has no caret API and fights formatting. ## npm ```sh npm install @devslab/numkey ``` ```ts import { format, parse, bind, observe } from '@devslab/numkey' format('1234567.5', { decimals: 2 }) // "1,234,567.5" parse('β© 1,234,567μ') // "1234567" bind(document.querySelector('#amount'), { decimals: 2 }) // one element observe() // all [data-numkey] ``` ## Vue 3 ```vue