@canutin/svelte-currency-input
v1.1.1
Published
A fully-featured currency input component for Svelte 5 that handles formatting, localization, and validation
Maintainers
Readme
svelte-currency-input
A fully-featured currency input component for Svelte 5 that handles formatting, localization, and validation as you type.

- Features
- Installation
- Usage
- How it works
- API
- Examples
- Styling
- Exported utilities
- Svelte 4 / migration guide
- Contributing
Features
- Formats positive and negative values
- Leverages
Intl.NumberFormatfor localizing currency denominations - Supports abbreviations (k, m, b) for quick input
- Configurable decimal precision with multiple control options
- Min/max constraints with arrow key stepping
- Custom prefix and suffix support
- Zero built-in styles — works with Tailwind, vanilla CSS, or any framework
- Simple, single
<input>element (no wrapper) - Full TypeScript support
- Lightweight — ~3.6KB gzipped with no runtime dependencies
- API and logic heavily inspired by @cchanxzy's react-currency-input-field
Installation
# bun
bun add @canutin/svelte-currency-input
# pnpm
pnpm add @canutin/svelte-currency-input
# npm
npm install @canutin/svelte-currency-input
# yarn
yarn add @canutin/svelte-currency-inputUsage
<script lang="ts">
import { CurrencyInput } from '@canutin/svelte-currency-input';
let value = $state('1234.56');
</script>
<CurrencyInput bind:value intlConfig={{ locale: 'en-US', currency: 'USD' }} />The input displays $1,234.56 while value contains the raw string "1234.56".
How it works
The component renders a single <input> element. The value prop is a string representing the unformatted number:
""= empty input"0"= zero"1234.56"= the number 1234.56
The formatted display (e.g., $1,234.56) is handled internally. For form submissions where you need the raw value, you can add a hidden input:
<form>
<CurrencyInput bind:value name="display" />
<input type="hidden" name="amount" {value} />
</form>API
Props
| Prop | Type | Default | Description |
| ------------------------ | --------------------------------------- | ----------- | ------------------------------------------------------------------------------- |
| value | string | '' | Bindable raw value (e.g., "1234.56") |
| intlConfig | IntlConfig | undefined | Locale and currency configuration |
| prefix | string | From locale | Override the currency prefix |
| suffix | string | '' | Override the currency suffix |
| decimalSeparator | string | From locale | Override the decimal separator |
| groupSeparator | string | From locale | Override the grouping separator |
| disableGroupSeparators | boolean | false | Disable thousand separators |
| allowDecimals | boolean | true | Allow decimal values |
| decimalsLimit | number | 2 | Max decimal digits while typing |
| decimalScale | number | undefined | Pad/trim decimals on blur |
| fixedDecimalLength | number | undefined | Fixed decimal input (e.g., 2: typing 123 → 1.23) |
| allowNegativeValue | boolean | true | Allow negative values |
| min | number | undefined | Minimum value (enforced on arrow key step) |
| max | number | undefined | Maximum value (enforced on arrow key step) |
| maxLength | number | undefined | Max characters (excluding formatting) |
| step | number | undefined | Arrow key increment/decrement |
| disableAbbreviations | boolean | false | Disable k/m/b abbreviations |
| formatValueOnBlur | boolean | true | Apply formatting when input loses focus |
| transformRawValue | (value: string) => string | undefined | Transform the raw value before processing |
| oninputvalue | (values: CurrencyInputValues) => void | undefined | Callback on every input change |
| onchangevalue | (values: CurrencyInputValues) => void | undefined | Callback on blur/commit |
| ref | HTMLInputElement \| null | null | Bindable reference to the input element |
| class | string | undefined | CSS class(es) for the input |
| ...restProps | HTMLInputAttributes | — | All standard input attributes (id, name, placeholder, disabled, required, etc.) |
Types
interface IntlConfig {
locale: string;
currency?: string;
// Also accepts other Intl.NumberFormatOptions
}
interface CurrencyInputValues {
float: number | null; // Parsed number or null if empty
formatted: string; // Display value: "$1,234.56"
value: string; // Raw value: "1234.56"
}Examples
International currencies
<CurrencyInput bind:value intlConfig={{ locale: 'de-DE', currency: 'EUR' }} />
<!-- Displays: 1.234,56 € -->Decimal precision
<!-- Limit to 2 decimals while typing, pad to 2 on blur -->
<CurrencyInput
bind:value
intlConfig={{ locale: 'en-US', currency: 'USD' }}
decimalsLimit={2}
decimalScale={2}
/>Min, max, and step
<!-- Use arrow keys to increment/decrement by 10, constrained to 0-100 -->
<CurrencyInput
bind:value
intlConfig={{ locale: 'en-US', currency: 'USD' }}
min={0}
max={100}
step={10}
/>Custom prefix and suffix
<!-- Points system -->
<CurrencyInput bind:value suffix=" pts" decimalsLimit={0} />
<!-- Bitcoin -->
<CurrencyInput bind:value prefix="₿ " decimalsLimit={8} />Abbreviations
Type 1k, 2.5m, or 1b to quickly enter large numbers:
<CurrencyInput
bind:value
intlConfig={{ locale: 'en-US', currency: 'USD' }}
placeholder="Try 1k, 2.5m, or 1b"
/>
<!-- Typing "2.5m" results in value="2500000" -->Callbacks
<CurrencyInput
bind:value
intlConfig={{ locale: 'en-US', currency: 'USD' }}
oninputvalue={({ float, formatted, value }) => {
console.log('On input:', { float, formatted, value });
}}
onchangevalue={({ float, formatted, value }) => {
console.log('On blur:', { float, formatted, value });
}}
/>Input element reference
<script>
let inputRef = $state(null);
function focusInput() {
inputRef?.focus();
}
</script>
<CurrencyInput bind:ref={inputRef} bind:value />
<button onclick={focusInput}>Focus</button>Styling
The component renders a single <input> element with no built-in styles. You can use the class prop to style it:
<!-- Tailwind CSS -->
<CurrencyInput bind:value class="rounded border px-3 py-2 focus:ring-2 focus:ring-blue-500" />
<!-- Custom CSS class -->
<CurrencyInput bind:value class="my-currency-input" />For dynamic styling based on value (positive/negative/zero), use the callback:
<script>
let value = $state('');
let colorClass = $state('');
function updateStyle({ float }) {
if (float === null || float === 0) colorClass = 'text-gray-500';
else if (float > 0) colorClass = 'text-green-600';
else colorClass = 'text-red-600';
}
</script>
<CurrencyInput bind:value class="border px-3 py-2 {colorClass}" oninputvalue={updateStyle} />Exported utilities
The package exports utility functions for use outside the component:
import {
formatValue,
getLocaleConfig,
cleanValue,
parseAbbrValue,
abbrValue
} from '@canutin/svelte-currency-input';
// Format a value with locale
const formatted = formatValue({
value: '1234.56',
intlConfig: { locale: 'en-US', currency: 'USD' }
});
// → "$1,234.56"
// Get locale configuration
const config = getLocaleConfig({ locale: 'de-DE', currency: 'EUR' });
// → { decimalSeparator: ',', groupSeparator: '.', prefix: '', suffix: ' €', ... }
// Parse abbreviations
const expanded = parseAbbrValue('2.5m', 'en-US');
// → "2500000"formatValue options
| Option | Type | Default | Description |
| ------------------------ | ------------ | ----------- | --------------------------------------------- |
| value | string | — | The value to format |
| intlConfig | IntlConfig | undefined | Locale and currency configuration |
| decimalScale | number | undefined | Number of decimal places |
| decimalSeparator | string | From locale | Override the decimal separator |
| groupSeparator | string | From locale | Override the grouping separator |
| disableGroupSeparators | boolean | false | Disable thousand separators |
| prefix | string | From locale | Override the currency prefix |
| suffix | string | '' | Override the currency suffix |
| roundValue | boolean | false | Round to decimalScale instead of truncating |
By default, formatValue truncates decimals to decimalScale. Set roundValue: true to round instead, which is useful when formatting computed values with floating-point precision artifacts:
// Default: truncates
formatValue({
value: '87.5',
intlConfig: { locale: 'en-US', currency: 'USD' },
decimalScale: 0
});
// → "$87"
// With roundValue: rounds
formatValue({
value: '87.5',
intlConfig: { locale: 'en-US', currency: 'USD' },
decimalScale: 0,
roundValue: true
});
// → "$88"
// Useful for computed values with floating-point issues
const sum = 500.75 + 300.5 - 200.25 - 150.33; // = 450.66999999999996
formatValue({
value: String(sum),
intlConfig: { locale: 'en-US', currency: 'USD' },
decimalScale: 2,
roundValue: true
});
// → "$450.67" (without roundValue, would be "$450.66")Svelte 4
For Svelte 4 support, use the 0.x version:
npm install @canutin/svelte-currency-input@0If you're upgrading from v0.x, see the migration guide.
Contributing
See CONTRIBUTING.md for development setup, testing, and contribution guidelines.
