npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

raqam

v0.4.2

Published

The definitive React number input: live formatting, full i18n, headless, accessible

Readme

Raqam 🔢

The definitive React number input: live formatting, full i18n, headless, accessible.

npm version bundle size CI TypeScript license

✨ Why raqam?

| Feature | Base UI | React Aria | Mantine | raqam | |---------|:-------:|:----------:|:-------:|:---------:| | Live formatting while typing | ❌ blur | ❌ blur | ✅ | ✅ | | Truly headless | ✅ | ✅ | ❌ | ✅ | | i18n digit input (Persian ۱۲۳, Arabic ١٢٣…) | ❌ | ✅ | ❌ | ✅ | | WAI-ARIA spinbutton | ✅ | ✅✅ | ⚠️ | ✅✅ | | Bundle size | ~10 KB | ~30 KB | ~60 KB | ~2.2 KB core |

No existing package combines all four. raqam does.

📦 Installation

npm install raqam
# or
pnpm add raqam

Peer dependencies: React 18 or 19.

🚀 Quick start

Hook API

import { useNumberFieldState, useNumberField, type UseNumberFieldStateOptions } from 'raqam'
import { useRef } from 'react'

function PriceInput() {
  // Share one options object — useNumberField builds its own formatter/parser,
  // so it needs the same formatting options as useNumberFieldState. `satisfies`
  // keeps the literal types (e.g. style: 'currency') in a strict TS project.
  const options = {
    locale: 'en-US',
    formatOptions: { style: 'currency', currency: 'USD' },
    minValue: 0,
    defaultValue: 1234.56,
  } satisfies UseNumberFieldStateOptions
  const state = useNumberFieldState(options)
  const inputRef = useRef(null)
  const { inputProps, labelProps, incrementButtonProps, decrementButtonProps } =
    useNumberField({ ...options, label: 'Price' }, state, inputRef)

  return (
    <div>
      <label {...labelProps}>Price</label>
      <button {...decrementButtonProps}>−</button>
      <input ref={inputRef} {...inputProps} />
      <button {...incrementButtonProps}>+</button>
    </div>
  )
}

Headless Component API

import { NumberField } from 'raqam'

function PriceField() {
  return (
    <NumberField.Root
      locale="en-US"
      formatOptions={{ style: 'currency', currency: 'USD' }}
      defaultValue={1234.56}
      minValue={0}
      onValueChange={(value, { reason }) => console.log(value, reason)}
    >
      <NumberField.Label>Price</NumberField.Label>
      <NumberField.Group>
        <NumberField.Decrement>−</NumberField.Decrement>
        <NumberField.Input />
        <NumberField.Increment>+</NumberField.Increment>
      </NumberField.Group>
      <NumberField.Description>Enter the product price</NumberField.Description>
      <NumberField.ErrorMessage />
    </NumberField.Root>
  )
}

🎨 Format presets

import { presets, NumberField } from 'raqam'

<NumberField.Root formatOptions={presets.currency('USD')} />           // $1,234.56
<NumberField.Root formatOptions={presets.accounting('USD')} />         // (1,234.56)
<NumberField.Root formatOptions={presets.percent} />                   // 12.3%
<NumberField.Root formatOptions={presets.compact} />                   // 1.2K
<NumberField.Root formatOptions={presets.scientific} />                // 1.23E3
<NumberField.Root formatOptions={presets.integer} />                   // 1,234
<NumberField.Root formatOptions={presets.financial} fixedDecimalScale /> // 1,234.00
<NumberField.Root formatOptions={presets.unit('kilometer-per-hour')} /> // 120 km/h

🌍 Locales & i18n

Persian input with native digits — just import the plugin and set the locale:

import 'raqam/locales/fa'  // registers ۰–۹ digit normalization (< 200 B)
import { NumberField } from 'raqam'

<NumberField.Root
  locale="fa-IR"
  formatOptions={{ style: 'currency', currency: 'IRR' }}
  suffix=" تومان"
/>
// user types ۱۲۳۴, raqam parses and formats it correctly in real-time

Supported scripts: 🇮🇷 Persian fa, 🇸🇦 Arabic ar, 🇧🇩 Bengali bn, 🇮🇳 Hindi hi, 🇹🇭 Thai th. RTL is auto-detected and handled.

✅ Custom validation

<NumberField.Root
  minValue={0}
  validate={(value) => {
    if (value === null) return 'Required'
    if (value % 2 !== 0) return 'Must be an even number'
    return true
  }}
>
  <NumberField.Input />
  <NumberField.ErrorMessage /> {/* auto-renders the validate() error string */}
</NumberField.Root>

👁️ Display-only formatting

import { useNumberFieldFormat } from 'raqam'

function PriceDisplay({ price }: { price: number }) {
  const formatted = useNumberFieldFormat(price, {
    locale: 'en-US',
    formatOptions: { style: 'currency', currency: 'USD' },
  })
  return <span>{formatted}</span>  // "$1,234.56"
}

Works in React Server Components too via raqam/server:

import { createFormatter } from 'raqam/server'  // zero React deps

const formatter = createFormatter({
  locale: 'en-US',
  formatOptions: { style: 'currency', currency: 'USD' },
})
const displayPrice = formatter.format(1234.56)  // "$1,234.56"

SSR / hydration notes

  • Pin locale for SSR. With no locale, formatting uses the runtime default — the browser locale on the client but the host's ICU/OS locale on the server. If they differ, the server-rendered value won't match the first client render and React logs a hydration mismatch. Pass an explicit locale (the same on both sides) whenever you server-render an initial value.
  • Label/description ARIA wires up after mount. <NumberField.Label> is associated with the input via the native htmlFor/id in the SSR HTML (screen readers honor it), but the redundant aria-labelledby (and aria-describedby for <NumberField.Description>) are attached on the client after the label/description registers — they appear post-hydration, not in the static HTML.

🖱️ ScrubArea (drag to change value)

<NumberField.Root defaultValue={50} minValue={0} maxValue={100}>
  <NumberField.ScrubArea direction="horizontal" pixelSensitivity={2}>
    <NumberField.Label>Opacity</NumberField.Label>
    <NumberField.ScrubAreaCursor>⟺</NumberField.ScrubAreaCursor>
  </NumberField.ScrubArea>
  <NumberField.Input />
</NumberField.Root>

Uses the Pointer Lock API so the cursor never hits the screen edge during drag.

💄 CSS styling with data attributes

/* All state-based styling — no JS needed */
[data-focused]   { outline: 2px solid blue; }
[data-invalid]   { border-color: red; }
[data-disabled]  { opacity: 0.5; }
[data-readonly]  { background: #f5f5f5; }
[data-required]  { /* required-field styling */ }
[data-scrubbing] { cursor: ew-resize; }
[data-rtl]       { /* RTL-specific overrides (set on the input element) */ }

data-focused, data-invalid, data-disabled, data-readonly, data-required, and data-scrubbing are set on NumberField.Root; data-rtl (plus the state attributes) is set on the NumberField.Input element.

🔗 react-hook-form integration

import { Controller } from 'react-hook-form'
import { NumberField } from 'raqam'

<Controller
  name="price"
  control={control}
  render={({ field, fieldState }) => (
    <NumberField.Root
      value={field.value}
      onChange={field.onChange}
      onBlur={field.onBlur}
      validate={() => fieldState.error?.message ?? true}
    >
      <NumberField.Label>Price</NumberField.Label>
      <NumberField.Input />
      <NumberField.ErrorMessage />
    </NumberField.Root>
  )}
/>

⚡ Arbitrary-precision string mode

For financial apps that need to avoid IEEE 754 float rounding:

<NumberField.Root
  onRawChange={(rawValue) => {
    // rawValue is the unformatted, precision-preserving numeric string
    // (grouping / currency / prefix / suffix stripped, locale decimal
    // normalized to ".", typed trailing zeros kept) — full precision beyond
    // JS float. e.g. "0.1000000001" — feed it to your BigDecimal library
    myDecimal.set(rawValue)
  }}
/>

Also available as state.rawValue from the hook API.

🔧 Custom formatter / parser

import Decimal from 'decimal.js'

<NumberField.Root
  formatValue={(value) => new Decimal(value).toFixed(8)}
  parseValue={(input) => {
    try {
      return { value: new Decimal(input).toNumber(), isIntermediate: false }
    } catch {
      return { value: null, isIntermediate: input.endsWith('.') }
    }
  }}
/>

📐 API Reference

useNumberFieldState(options)

State management hook — returns NumberFieldState.

| Prop | Type | Default | Description | |------|------|---------|-------------| | value | number \| null | — | Controlled value | | defaultValue | number \| null | — | Uncontrolled default | | onChange | (value: number \| null) => void | — | Fires whenever the parsed numeric value changes | | onRawChange | (raw: string \| null) => void | — | Fires with raw unformatted string | | locale | string | browser | BCP 47 locale tag | | formatOptions | Intl.NumberFormatOptions | {} | Full Intl options | | minValue | number | — | Minimum value | | maxValue | number | — | Maximum value | | step | number | 1 | Arrow key step | | largeStep | number | step × 10 | Shift+Arrow step | | smallStep | number | step × 0.1 | Ctrl/Meta+Arrow step | | clampBehavior | "blur" \| "strict" \| "none" | "blur" | When to clamp to min/max | | allowNegative | boolean | true | Allow negative values | | allowDecimal | boolean | true | Allow decimal values | | fixedDecimalScale | boolean | false | Always show max decimal places | | allowOutOfRange | boolean | false | Skip clamping (server-side validation) | | validate | (v: number \| null) => boolean \| string \| null | — | Custom validation | | prefix | string | — | String prefix (e.g. "$") | | suffix | string | — | String suffix (e.g. " تومان") | | liveFormat | boolean | true | Format while typing (disable for IME locales) | | disabled | boolean | false | Disable the field | | readOnly | boolean | false | Read-only mode | | required | boolean | false | Mark the field as required |

Also accepts maximumFractionDigits, minimumFractionDigits, formatValue, and parseValue — see the full options reference.

useNumberField(props, state, inputRef)

Behavior hook — returns NumberFieldAria prop objects for each element. Accepts every useNumberFieldState option plus the behavior-only props below:

| Prop | Type | Default | Description | |------|------|---------|-------------| | allowMouseWheel | boolean | false | Mouse wheel to increment/decrement | | copyBehavior | "formatted" \| "raw" \| "number" | "formatted" | Clipboard content on copy | | stepHoldDelay | number | 400 | Press-and-hold initial delay (ms) | | stepHoldInterval | number | 200 | Press-and-hold repeat interval (ms) | | label / name / id / aria-* | string | — | Labelling, form name, and id wiring |

→ Full reference: useNumberField.

NumberField.Root extra props

| Prop | Type | Description | |------|------|-------------| | onValueChange | (value, { reason, formattedValue }) => void | Fires on every change, with the change reason | | onValueCommitted | (value, { reason }) => void | Fires only on commit (reason: "blur" \| "keyboard") | | className / style | string / CSSProperties | Applied to the root wrapper <div> |

useNumberFieldFormat(value, options)

Display-only formatting hook (client-only — it carries "use client"). Returns a formatted string with zero state overhead. For React Server Components, SSR, or Edge, use createFormatter from raqam/server instead (shown above).

NumberField.* components

| Component | Description | |-----------|-------------| | Root | Context provider + state orchestration | | Label | <label> with correct htmlFor wiring | | Group | <div role="group"> for input + buttons | | Input | <input type="text" role="spinbutton"> with live formatting | | Increment | Increment button with press-and-hold acceleration | | Decrement | Decrement button with press-and-hold acceleration | | HiddenInput | Hidden <input> for native FormData submission | | ScrubArea | Pointer Lock drag-to-adjust area | | ScrubAreaCursor | Custom cursor rendered during pointer lock | | Description | Help text linked via aria-describedby | | ErrorMessage | Error display with role="alert" | | Formatted | Read-only formatted value display span |

Every component accepts a render prop for element replacement:

<NumberField.Increment render={<MyIconButton />}>▲</NumberField.Increment>
// or with state access:
<NumberField.Increment render={(props, state) => (
  <MyBtn disabled={!state.canIncrement} {...props} />
)} />

📦 Bundle size

Measured min + brotli (including dependencies), enforced in CI via .size-limit.json:

| Entry | Size | CI budget | |-------|------|-----------| | raqam/core | ~2.23 KB | 2.5 KB | | raqam (hooks + components) | ~9.62 KB | 12 KB | | raqam/react | ~9.42 KB | 10 KB | | raqam/locales/fa | 196 B | 0.3 KB |

📄 License

MIT