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.
⨠Two Specialized Components
šÆ Developer Experience
bind:valueš Internationalization
Intl.NumberFormat APICheck out the working demo: https://pitis.github.io/svelte-number-format/
npm install svelte-number-format
<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"
/>
<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"
/>
Locale-aware number formatting built on intl-number-input.
| Prop | Type | Default | Description |
|---|---|---|---|
value |
number | null |
null |
The numeric value. Use bind:value for two-way binding. |
locale |
string |
navigator.language |
Locale string for formatting (e.g., 'en-US', 'de-DE', 'ja-JP') |
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 |
...rest |
any |
- | All other HTML input attributes (placeholder, class, id, etc.) |
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 1234 ā 12.34) |
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%)
<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 -->
<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 -->
<NumericFormat
bind:value={amount}
locale="de-DE"
options={{
formatStyle: NumberFormatStyle.Currency,
currency: 'EUR',
precision: 2
}}
/>
<!-- User sees: 1.234,56 ⬠-->
<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 -->
<NumericFormat
bind:value={amount}
options={{
precision: 2,
valueRange: { min: 0, max: 1000 }
}}
placeholder="0 - 1000"
/>
<!-- Values are clamped to 0-1000 on blur -->
<NumericFormat
bind:value={price}
options={{
precision: 2,
autoDecimalDigits: true
}}
placeholder="Type 1234 ā 12.34"
/>
<!-- Typing "1234" automatically formats as "12.34" -->
<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}
/>
Pattern-based input masking for structured text inputs.
| 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 below. |
mask |
string |
'' |
Deprecated - Use format instead. Kept for backwards compatibility. |
maskChar |
string |
'_' |
Character shown in placeholder for pattern positions |
placeholder |
string |
auto | Placeholder text (auto-generated from format if not provided) |
onInput |
(raw: string | null, formatted: string | null) => void |
undefined |
Callback fired on every keystroke |
onChange |
(raw: string | null, formatted: string | null) => void |
undefined |
Callback fired on blur/change |
...rest |
any |
- | All other HTML input attributes |
| Character | Accepts | Example |
|---|---|---|
# |
Digit (0-9) | ### ā 123 |
A |
Letter (a-zA-Z) | AAA ā ABC |
* |
Alphanumeric (a-zA-Z0-9) | *** ā A1B |
| Other | Literal | -, (, ), /, :, etc. |
Import ready-to-use patterns:
import { MaskPatterns } from 'svelte-number-format'
MaskPatterns.PHONE_US // (###) ###-####
MaskPatterns.PHONE_US_WITH_EXT // (###) ###-#### ext. #####
MaskPatterns.PHONE_INTERNATIONAL // +## (###) ###-####
MaskPatterns.CREDIT_CARD // #### #### #### ####
MaskPatterns.CREDIT_CARD_AMEX // #### ###### #####
MaskPatterns.DATE_US // ##/##/####
MaskPatterns.DATE_ISO // ####-##-##
MaskPatterns.DATE_EU // ##.##.####
MaskPatterns.TIME_12H // ##:## AM
MaskPatterns.TIME_24H // ##:##
MaskPatterns.DATETIME_US // ##/##/#### ##:##
MaskPatterns.SSN // ###-##-####
MaskPatterns.ZIP_US // #####
MaskPatterns.ZIP_US_PLUS4 // #####-####
MaskPatterns.IPV4 // ###.###.###.###
MaskPatterns.MAC_ADDRESS // ##:##:##:##:##:##
MaskPatterns.HEX_COLOR // #******
<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" -->
<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" -->
<PatternFormat
bind:value={date}
format={MaskPatterns.DATE_US}
placeholder="MM/DD/YYYY"
/>
<!-- User types: 12252024 -->
<!-- Display: 12/25/2024 -->
<!-- Value stored: "12252024" -->
<PatternFormat bind:value={ssn} format={MaskPatterns.SSN} />
<!-- Display: 123-45-6789 -->
<!-- Value stored: "123456789" -->
<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" -->
<PatternFormat bind:value={plate} format="AAA ####" placeholder="ABC 1234" />
<PatternFormat bind:value={product} format="***-***-***" />
<!-- Accepts any combination of letters and numbers -->
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>
<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>
<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>
If you're upgrading from an earlier version, see MIGRATION.md for the full migration guide.
Old names (still work):
import {(SvelteNumberFormat, SvelteMaskFormat)} from 'svelte-number-format';
New names (recommended):
import {(NumericFormat, PatternFormat)} from 'svelte-number-format';
Full TypeScript support with proper type definitions:
import type { NumberInputOptions } from 'intl-number-input'
import {
NumericFormat,
PatternFormat,
NumberFormatStyle,
MaskPatterns
} from 'svelte-number-format'
import type { MaskPattern } from 'svelte-number-format'
Intl.NumberFormat supportContributions are welcome! This project uses:
Before each commit, the following runs automatically:
See CONTRIBUTING.md for detailed development setup and guidelines.
MIT Ā© Pitis Radu