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.2.3

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 | ~1.7 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 } from 'raqam'
import { useRef } from 'react'

function PriceInput() {
  const state = useNumberFieldState({
    locale: 'en-US',
    formatOptions: { style: 'currency', currency: 'USD' },
    minValue: 0,
    defaultValue: 1234.56,
  })
  const inputRef = useRef(null)
  const { inputProps, labelProps, incrementButtonProps, decrementButtonProps } =
    useNumberField({ 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"

🖱️ 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-rtl]       { /* RTL-specific overrides */ }
[data-scrubbing] { cursor: ew-resize; }

🔗 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 exact string before any JS float conversion
    // 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 | — | Uncontrolled default | | onChange | (value: number \| null) => void | — | Fires on every change | | 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. " تومان") | | disabled | boolean | false | Disable the field | | readOnly | boolean | false | Read-only mode |

useNumberField(props, state, inputRef)

Behavior hook — returns NumberFieldAria prop objects for each element.

Additional props beyond state options:

| 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) | | formatValue | (value: number) => string | — | Custom format function | | parseValue | (input: string) => ParseResult | — | Custom parse function |

NumberField.Root extra props

| Prop | Type | Description | |------|------|-------------| | onValueChange | (value, { reason, formattedValue }) => void | Fires with change reason | | onValueCommitted | (value, { reason }) => void | Fires only on blur/Enter |

useNumberFieldFormat(value, options)

Display-only formatting hook. Returns a formatted string. Zero state overhead — safe in RSC via raqam/server.

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

Actual sizes (brotli compressed):

| Entry | Size | |-------|------| | raqam/core | ~1.7 KB | | raqam (hooks + components) | ~7 KB | | raqam/react | ~6.8 KB | | raqam/locales/fa | ~200 B |

📄 License

MIT