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

svelte-number-format

v2.0.0

Published

Lightweight, reactive number and pattern formatting for Svelte 5. NumericFormat & PatternFormat for inputs, NumericText & PatternText for SSR-safe display.

Downloads

537

Readme

svelte-number-format

CI Deploy npm version

Svelte Number Format is a lightweight and reactive input component library for Svelte 5.
Inspired by react-number-format, it provides two powerful components for handling formatted inputs with full caret stability and two-way binding.

Features

Four Components

  • NumericFormat — locale-aware number input (currency, percentages, decimals)
  • PatternFormat — pattern-based input masking (phone, credit cards, dates, custom)
  • NumericText — display-only rendering of formatted numbers (no input)
  • PatternText — display-only rendering of masked strings (no input)

🎯 Developer experience

  • Full TypeScript support
  • Two-way binding with bind:value
  • Svelte 5 native (runes only)
  • Caret position stability across formatting
  • Paste handling that re-formats the whole clipboard buffer in one shot
  • IME (composition) aware — doesn't break CJK input
  • Rich onValueChange callback with { floatValue, formattedValue, value } (react-number-format compatible)
  • Custom pattern tokens via customPatterns
  • allowEmptyFormatting to show the mask skeleton before typing
  • SSR-safe (no navigator at module eval)
  • A11y-ready — forwards aria-* attributes, auto-sets aria-placeholder and inputmode

🌍 Internationalization

  • Built on Intl.NumberFormat
  • Any BCP-47 locale
  • Automatic thousands / decimal / currency symbol per locale

Live Demo

Check out the working demo: https://pitis.github.io/svelte-number-format/

Installation

npm install svelte-number-format

Quick Start

Currency Input

<script lang="ts">
  import { NumericFormat, NumberFormatStyle } from 'svelte-number-format'

  let amount = $state<number | null>(1234.56)
</script>

<NumericFormat
  bind:value={amount}
  locale="en-US"
  options={{
    formatStyle: NumberFormatStyle.Currency,
    currency: 'USD',
    precision: 2
  }}
  placeholder="$0.00"
/>

Phone Number Input

<script lang="ts">
  import { PatternFormat, MaskPatterns } from 'svelte-number-format'

  let phone = $state<string | null>(null)
</script>

<PatternFormat
  bind:value={phone}
  format={MaskPatterns.PHONE_US}
  placeholder="(123) 456-7890"
/>

NumericFormat Component

Locale-aware number formatting built on intl-number-input.

Props

| Prop | Type | Default | Description | | --------------- | ---------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------ | | value | number \| string \| null | null | The numeric value. Use bind:value for two-way binding. | | valueType | 'number' \| 'string' | 'number' | Whether the bound value is emitted as a number or a decimal string. | | locale | string \| undefined | resolved lazily | Locale string. Defaults to navigator.language on the client, 'en-US' during SSR. | | options | Partial<NumberInputOptions> | {} | Formatting options (see below). | | onInput | (raw: number \| null, formatted: string \| null) => void | undefined | Callback fired on every keystroke. | | onChange | (raw: number \| null, formatted: string \| null) => void | undefined | Callback fired on blur/change. | | onValueChange | (values: NumberFormatValues, source: SourceInfo) => void | undefined | Rich payload callback. See onValueChange. | | ...rest | any | — | All other HTML input attributes. |

Options

The options prop accepts these properties:

| Option | Type | Description | | ------------------- | -------------------------------- | ----------------------------------------------------------------------------- | | formatStyle | NumberFormatStyle | Decimal, Currency, or Percent | | currency | string | Currency code (e.g., 'USD', 'EUR', 'GBP') - required for Currency style | | precision | number | Number of decimal places | | valueRange | { min?: number, max?: number } | Min/max value constraints | | autoDecimalDigits | boolean | Automatically position decimal (e.g., typing 123412.34) |

NumberFormatStyle Enum

import { NumberFormatStyle } from 'svelte-number-format'

NumberFormatStyle.Decimal // Plain number with locale formatting
NumberFormatStyle.Currency // Currency with symbol ($, €, £, etc.)
NumberFormatStyle.Percent // Percentage (0.75 → 75%)

Examples

Basic Number Input

<script lang="ts">
  import { NumericFormat } from 'svelte-number-format'
  let value = $state<number | null>(1234.56)
</script>

<NumericFormat
  bind:value
  options={{ precision: 2 }}
  placeholder="Enter amount"
/>
<!-- User sees: 1,234.56 -->

Currency (USD)

<script lang="ts">
  import { NumericFormat, NumberFormatStyle } from 'svelte-number-format'
  let price = $state<number | null>(99.99)
</script>

<NumericFormat
  bind:value={price}
  locale="en-US"
  options={{
    formatStyle: NumberFormatStyle.Currency,
    currency: 'USD',
    precision: 2
  }}
/>
<!-- User sees: $99.99 -->

Currency (EUR with German locale)

<NumericFormat
  bind:value={amount}
  locale="de-DE"
  options={{
    formatStyle: NumberFormatStyle.Currency,
    currency: 'EUR',
    precision: 2
  }}
/>
<!-- User sees: 1.234,56 € -->

Percentage

<script lang="ts">
  import { NumericFormat, NumberFormatStyle } from 'svelte-number-format'
  let rate = $state<number | null>(0.75) // Store as decimal
</script>

<NumericFormat
  bind:value={rate}
  options={{
    formatStyle: NumberFormatStyle.Percent,
    precision: 2
  }}
/>
<!-- User sees: 75.00% -->
<!-- Value stored as: 0.75 -->

With Value Range

<NumericFormat
  bind:value={amount}
  options={{
    precision: 2,
    valueRange: { min: 0, max: 1000 }
  }}
  placeholder="0 - 1000"
/>
<!-- Values are clamped to 0-1000 on blur -->

Auto Decimal Mode

<NumericFormat
  bind:value={price}
  options={{
    precision: 2,
    autoDecimalDigits: true
  }}
  placeholder="Type 1234 → 12.34"
/>
<!-- Typing "1234" automatically formats as "12.34" -->

With Callbacks

<script lang="ts">
  import { NumericFormat } from 'svelte-number-format'

  let value = $state<number | null>(null)

  function handleInput(raw: number | null, formatted: string | null) {
    console.log('Input:', raw, formatted)
  }

  function handleChange(raw: number | null, formatted: string | null) {
    console.log('Change:', raw, formatted)
  }
</script>

<NumericFormat
  bind:value
  options={{ precision: 2 }}
  onInput={handleInput}
  onChange={handleChange}
/>

PatternFormat Component

Pattern-based input masking for structured text inputs.

Props

| Prop | Type | Default | Description | | ---------------------- | ---------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------- | | value | string \| null | null | The raw unmasked value. Use bind:value for two-way binding. | | format | string | '' | Pattern string (e.g. '(###) ###-####'). See pattern characters. | | mask | string | '' | Deprecated — use format. Emits a dev-mode warning. Removed in 2.0. | | maskChar | string | '_' | Character shown in auto-generated placeholder for pattern positions. | | placeholder | string | auto | Placeholder text. Auto-generated from format if not provided. | | customPatterns | Record<string, RegExp> | undefined | Additional pattern tokens. See Custom patterns. | | allowEmptyFormatting | boolean | false | Render the mask skeleton as the input's value when empty. See below. | | onInput | (raw: string \| null, formatted: string \| null) => void | undefined | Callback fired on every keystroke (not during IME composition). | | onChange | (raw: string \| null, formatted: string \| null) => void | undefined | Callback fired on blur/change. | | onValueChange | (values: NumberFormatValues, source: SourceInfo) => void | undefined | Rich payload callback. See onValueChange. | | ...rest | any | — | All other HTML input attributes. |

Pattern Characters

| Character | Accepts | Example | | --------- | ------------------------ | ----------------------------- | | # | Digit (0-9) | ###123 | | A | Letter (a-zA-Z) | AAAABC | | * | Alphanumeric (a-zA-Z0-9) | ***A1B | | Other | Literal | -, (, ), /, :, etc. |

Predefined Patterns

Import ready-to-use patterns:

import { MaskPatterns } from 'svelte-number-format'

Phone Numbers

MaskPatterns.PHONE_US // (###) ###-####
MaskPatterns.PHONE_US_WITH_EXT // (###) ###-#### ext. #####
MaskPatterns.PHONE_INTERNATIONAL // +## (###) ###-####

Credit Cards

MaskPatterns.CREDIT_CARD // #### #### #### ####
MaskPatterns.CREDIT_CARD_AMEX // #### ###### #####

Dates & Time

MaskPatterns.DATE_US // ##/##/####
MaskPatterns.DATE_ISO // ####-##-##
MaskPatterns.DATE_EU // ##.##.####
MaskPatterns.TIME_12H // ##:## AM
MaskPatterns.TIME_24H // ##:##
MaskPatterns.DATETIME_US // ##/##/#### ##:##

Identification

MaskPatterns.SSN // ###-##-####
MaskPatterns.ZIP_US // #####
MaskPatterns.ZIP_US_PLUS4 // #####-####

Other

MaskPatterns.IPV4 // ###.###.###.###
MaskPatterns.MAC_ADDRESS // ##:##:##:##:##:##
MaskPatterns.HEX_COLOR // #******

Examples

Phone Number

<script lang="ts">
  import { PatternFormat, MaskPatterns } from 'svelte-number-format'
  let phone = $state<string | null>(null)
</script>

<PatternFormat bind:value={phone} format={MaskPatterns.PHONE_US} />
<!-- User types: 1234567890 -->
<!-- Display: (123) 456-7890 -->
<!-- Value stored: "1234567890" -->

Credit Card

<script lang="ts">
  import { PatternFormat, MaskPatterns } from 'svelte-number-format'
  let card = $state<string | null>(null)
</script>

<PatternFormat
  bind:value={card}
  format={MaskPatterns.CREDIT_CARD}
  placeholder="1234 5678 9012 3456"
/>
<!-- User types: 1234567890123456 -->
<!-- Display: 1234 5678 9012 3456 -->
<!-- Value stored: "1234567890123456" -->

Date

<PatternFormat
  bind:value={date}
  format={MaskPatterns.DATE_US}
  placeholder="MM/DD/YYYY"
/>
<!-- User types: 12252024 -->
<!-- Display: 12/25/2024 -->
<!-- Value stored: "12252024" -->

Social Security Number

<PatternFormat bind:value={ssn} format={MaskPatterns.SSN} />
<!-- Display: 123-45-6789 -->
<!-- Value stored: "123456789" -->

Custom Pattern

<PatternFormat
  bind:value={code}
  format="AAA-###-***"
  placeholder="ABC-123-XYZ"
/>
<!-- Accepts: [Letter][Letter][Letter]-[Digit][Digit][Digit]-[Any][Any][Any] -->
<!-- Example: ABC-123-X5Z -->
<!-- Value stored: "ABC123X5Z" -->

License Plate (Custom)

<PatternFormat bind:value={plate} format="AAA ####" placeholder="ABC 1234" />

Product Code (Custom)

<PatternFormat bind:value={product} format="***-***-***" />
<!-- Accepts any combination of letters and numbers -->

Display-only components

NumericText and PatternText render formatted values as a <span> (no input). Useful in tables, summaries, and read-only views where you want to reuse your formatting rules.

<script lang="ts">
  import {
    NumericText,
    PatternText,
    MaskPatterns,
    NumberFormatStyle
  } from 'svelte-number-format'
</script>

<!-- Display currency -->
<NumericText
  value={1234.56}
  locale="en-US"
  options={{
    formatStyle: NumberFormatStyle.Currency,
    currency: 'USD',
    precision: 2
  }}
  class="price"
/>
<!-- renders: <span class="price">$1,234.56</span> -->

<!-- Display formatted phone -->
<PatternText value="4155551234" format={MaskPatterns.PHONE_US} />
<!-- renders: <span>(415) 555-1234</span> -->

<!-- Fallback when value is null -->
<NumericText value={null} fallback="—" />

Both components accept a fallback prop for null/empty values.


onValueChange rich payload

For parity with react-number-format, both inputs accept an onValueChange callback with a structured payload:

interface NumberFormatValues {
  floatValue: number | undefined // parsed number, or undefined when empty/invalid
  formattedValue: string // what the user sees in the input
  value: string // raw string representation (e.g. "1234.56")
}

interface SourceInfo {
  event: Event | undefined
  source: 'event' | 'prop' // 'prop' if triggered by external value change
}
<script lang="ts">
  import { NumericFormat, type NumberFormatValues } from 'svelte-number-format'

  let amount = $state<number | null>(null)

  function handleValueChange(values: NumberFormatValues) {
    console.log(values.floatValue) // 1234.56
    console.log(values.formattedValue) // "$1,234.56"
    console.log(values.value) // "1234.56"
  }
</script>

<NumericFormat
  bind:value={amount}
  options={{ precision: 2 }}
  onValueChange={handleValueChange}
/>

Pick the field that matches your form-library's expectations — floatValue for Zod z.number(), value for string schemas, formattedValue for display.


Paste handling

PatternFormat correctly handles paste in one shot, not character-by-character. Pasting any string (including pre-formatted input like (415) 555-1234 into a phone mask) strips non-matching characters and re-applies the mask atomically, placing the cursor where expected.

No configuration needed — it just works.


Custom patterns

Add your own token characters for patterns that don't fit # / A / *:

<script lang="ts">
  import { PatternFormat } from 'svelte-number-format'

  let hexColor = $state<string | null>(null)
</script>

<PatternFormat
  bind:value={hexColor}
  format="HHHHHH"
  customPatterns={{ H: /[0-9a-fA-F]/ }}
  placeholder="ff00aa"
/>

<!-- Binary -->
<PatternFormat format="BBBB BBBB" customPatterns={{ B: /[01]/ }} />

Keys that collide with the built-in tokens (#, A, *) trigger a dev-mode warning and the built-in takes precedence.


allowEmptyFormatting

Show the mask skeleton in the input even when empty, so users see the expected shape before they start typing:

<PatternFormat format={MaskPatterns.PHONE_US} allowEmptyFormatting />
<!-- input.value = "(___) ___-____" even with no value -->

On focus, the caret lands at the first fillable slot.


Accessibility

Both input components forward all HTML attributes via spread, so aria-invalid, aria-describedby, aria-label, aria-errormessage, and role work out of the box. In addition:

  • PatternFormat sets aria-placeholder to the auto-generated mask string (e.g. (___) ___-____) so screen readers announce the expected shape.
  • PatternFormat auto-infers inputmode from the pattern (numeric / tel / text) to trigger the right mobile keyboard. Consumer-supplied inputmode wins.
  • NumericFormat gets its inputmode from the underlying formatter (decimal by default).
<PatternFormat
  format={MaskPatterns.PHONE_US}
  aria-label="Phone number"
  aria-invalid={!phone}
  aria-describedby="phone-error"
/>

SSR / SvelteKit

Both components render safely in SSR. NumericFormat's default locale is resolved lazily — navigator.language on the client, 'en-US' during server render — so +page.svelte using these components won't throw on the server.

<!-- +page.svelte (SSR-safe) -->
<script lang="ts">
  import { NumericFormat } from 'svelte-number-format'
  let price = $state<number | null>(19.99)
</script>

<NumericFormat bind:value={price} locale="en-US" options={{ precision: 2 }} />

Pass an explicit locale prop if you want consistent server/client rendering regardless of the visitor's browser language.


Subpath imports

For more explicit tree-shaking, import from narrow subpaths:

// Numeric only (skips loading pattern masking code)
import { NumericFormat, NumericText } from 'svelte-number-format/numeric'

// Pattern only (skips loading intl-number-input)
import { PatternFormat, PatternText } from 'svelte-number-format/pattern'

// Just the patterns constant
import { MaskPatterns } from 'svelte-number-format/patterns'

// Just display-only components
import { NumericText, PatternText } from 'svelte-number-format/display'

The root export (svelte-number-format) still works and includes everything. Modern bundlers tree-shake the root export correctly, but subpath imports are clearer about intent.


Form-library integration

Both inputs use plain bind:value on a number (NumericFormat) or a string of raw digits (PatternFormat), which makes them drop-in compatible with the popular Svelte form libraries. The playground has three worked examples — source in src/routes/demos/:

All three share one schema:

import { z } from 'zod'

export const schema = z.object({
  amount: z.number().min(0).max(1_000_000),
  phone: z.string().regex(/^\d{10}$/, 'Must be exactly 10 digits')
})

Superforms (the standard choice)

<script lang="ts">
  import { superForm } from 'sveltekit-superforms'
  import {
    NumericFormat,
    PatternFormat,
    MaskPatterns,
    NumberFormatStyle
  } from 'svelte-number-format'

  let { data } = $props()
  const { form, errors, enhance } = superForm(data.form, { dataType: 'json' })
</script>

<form method="POST" use:enhance>
  <NumericFormat
    bind:value={$form.amount}
    options={{
      formatStyle: NumberFormatStyle.Currency,
      currency: 'USD',
      precision: 2
    }}
  />
  {#if $errors.amount}<p class="error">{$errors.amount}</p>{/if}

  <PatternFormat bind:value={$form.phone} format={MaskPatterns.PHONE_US} />
  {#if $errors.phone}<p class="error">{$errors.phone}</p>{/if}

  <button type="submit">Submit</button>
</form>

Felte (client-side)

<script lang="ts">
  import { createForm } from 'felte'
  import { validator } from '@felte/validator-zod'
  import {
    NumericFormat,
    PatternFormat,
    MaskPatterns,
    NumberFormatStyle
  } from 'svelte-number-format'

  let amount = $state<number | null>(0)
  let phone = $state<string | null>('')

  const { form, errors, setFields } = createForm({
    initialValues: { amount: 0, phone: '' },
    extend: [validator({ schema })],
    onSubmit: (values) => {
      /* ... */
    }
  })

  $effect(() => {
    setFields('amount', amount ?? 0, true)
  })
  $effect(() => {
    setFields('phone', phone ?? '', true)
  })
</script>

<form use:form>
  <NumericFormat
    name="amount"
    bind:value={amount}
    options={{
      formatStyle: NumberFormatStyle.Currency,
      currency: 'USD',
      precision: 2
    }}
  />
  <PatternFormat
    name="phone"
    bind:value={phone}
    format={MaskPatterns.PHONE_US}
  />
</form>

The Felte integration needs a tiny $effect bridge because Felte's internal store is string-keyed form data populated by DOM name=… attributes, while our components emit typed values via bind:value. Both Superforms and Formsnap avoid this because they already consume a reactive store.


Migrating from react-number-format

svelte-number-format mirrors react-number-format's API where possible. The big differences come from Svelte's idioms rather than missing features.

| react-number-format | svelte-number-format | Notes | | -------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------ | | <NumericFormat /> | <NumericFormat /> | Same name, same concept. | | <PatternFormat /> | <PatternFormat /> | Same name, same concept. | | <NumericFormat displayType="text" /> | <NumericText /> | Separate component instead of a prop. | | <PatternFormat displayType="text" /> | <PatternText /> | Same idea for pattern masks. | | value={state} + onValueChange | bind:value={state} or onValueChange | Use Svelte's bind:value — simpler, no need to wire state. | | onValueChange={(v) => ...} | onValueChange={(v, s) => ...} | Payload shape is the same: { floatValue, formattedValue, value }. | | format="(###) ###-####" | format="(###) ###-####" | Same token characters (#, but not A/* in react-number-format's default build). | | format with custom patterns | customPatterns={{ H: /[0-9a-f]/ }} | Pass the regex map as a prop. | | allowEmptyFormatting | allowEmptyFormatting | Same semantics. | | mask="_" | maskChar="_" | Renamed to avoid collision with the legacy mask prop. | | thousandSeparator | options.useGrouping | Locale-aware — set locale instead of separators. | | decimalSeparator | Locale-aware | Set locale="de-DE" to get 1.234,56. | | thousandsGroupStyle | Locale-aware | Locales handle grouping (en-IN1,23,456). | | prefix / suffix | options.formatStyle: Currency | For currency, use formatStyle + currency for locale-correct symbols. | | valueIsNumericString | valueType="string" | Emits the bound value as a string when set. |


Advanced Usage

Controlled Components

Both components support controlled mode:

<script lang="ts">
  import { NumericFormat } from 'svelte-number-format'
  let amount = $state<number | null>(100)
</script>

<NumericFormat bind:value={amount} options={{ precision: 2 }} />

<button onclick={() => (amount = 100)}>$100</button>
<button onclick={() => (amount = 1000)}>$1,000</button>
<button onclick={() => (amount = null)}>Clear</button>

Form Integration

<script lang="ts">
  let formData = $state({
    price: null as number | null,
    phone: null as string | null
  })

  function handleSubmit() {
    console.log('Form data:', formData)
  }
</script>

<form onsubmit={handleSubmit}>
  <NumericFormat
    bind:value={formData.price}
    options={{ formatStyle: NumberFormatStyle.Currency, currency: 'USD' }}
  />

  <PatternFormat bind:value={formData.phone} format={MaskPatterns.PHONE_US} />

  <button type="submit">Submit</button>
</form>

Custom Styling

<NumericFormat
  bind:value={amount}
  class="my-custom-input"
  style="border: 2px solid blue;"
/>

<style>
  :global(.my-custom-input) {
    padding: 1rem;
    font-size: 1.5rem;
    border-radius: 8px;
  }
</style>

Migration from v1.x

See MIGRATION.md for the full guide.

v1.x → v2.0 breaking changes

v2.0 removes three long-deprecated APIs. None of them have functional replacements you don't already have — it's pure cleanup.

| Removed | Replacement | Deprecated since | | --------------------------------------------------- | ------------------------------ | ---------------- | | SvelteNumberFormat (re-export of NumericFormat) | NumericFormat | 1.0 | | SvelteMaskFormat (re-export of PatternFormat) | PatternFormat | 1.0 | | <PatternFormat mask="###"> prop | <PatternFormat format="###"> | 1.0 |

<!-- v1.x -->
<script>
  import { SvelteMaskFormat } from 'svelte-number-format'
</script>
<SvelteMaskFormat mask="(###) ###-####" />

<!-- v2.0 -->
<script>
  import { PatternFormat } from 'svelte-number-format'
</script>
<PatternFormat format="(###) ###-####" />

The v1.2 dev-mode warning for the mask prop is removed in v2.0 along with the prop itself.


TypeScript

Full TypeScript support with proper type definitions:

import type { NumberInputOptions } from 'intl-number-input'
import {
  NumericFormat,
  PatternFormat,
  NumericText,
  PatternText,
  NumberFormatStyle,
  MaskPatterns
} from 'svelte-number-format'
import type {
  MaskPattern,
  NumberFormatValues,
  OnValueChange,
  SourceInfo,
  ValueChangeSource
} from 'svelte-number-format'

Browser Support

  • Svelte 5+
  • Modern browsers with Intl.NumberFormat support
  • IE11+ with polyfills

Contributing

Contributions are welcome! This project uses:

  • Husky - Git hooks for quality checks
  • lint-staged - Run checks on staged files only
  • Pre-commit hooks - Automatic formatting, linting, and testing

Before each commit, the following runs automatically:

  • ✅ Prettier formatting
  • ✅ ESLint linting with auto-fix
  • ✅ Tests for changed files

See CONTRIBUTING.md for detailed development setup and guidelines.


License

MIT © Pitis Radu


Acknowledgments