Currency Input
stimeo--currency-input
An amount field that groups digits as you type while submitting a clean number.
Groups digits as you type and settles the decimal places when you leave the field. What you typed survives: a leading minus, a decimal mark you are still typing past, and the fraction digits are never eaten mid-keystroke, and the caret stays where you put it. Full-width digits confirmed by a Japanese input method are converted rather than dropped, and text still being composed is left alone. The field shows the grouped amount while the value submitted is the plain number, which is also what screen readers are told. A currency or precision setting it cannot read falls back to a default rather than breaking the field. It brings no styling and no validation.
Keyboard
This component has no keyboard interactions of its own.
<%# Markup for the currency-input demo.
stimeo--currency-input formats thousands separators (while typing / on blur) and
rounds decimals, keeping the normalized number in a submit hidden input. It
separates the formatted display from the machine-readable value, and has assistive
tech announce the normalized actual value (srValue) via aria-describedby. %>
<div
class="currency-input"
data-controller="stimeo--currency-input"
data-stimeo--currency-input-locale-value="<%= t("components.currency_input.demo.locale") %>"
data-stimeo--currency-input-currency-value="<%= t("components.currency_input.demo.currency") %>"
data-stimeo--currency-input-precision-value="<%= t(
"components.currency_input.demo.precision"
) %>">
<label class="currency-input__label" for="currency-amount">
<%= t("components.currency_input.demo.label") %>
</label>
<div class="currency-input__field">
<span class="currency-input__symbol" aria-hidden="true">
<%= t("components.currency_input.demo.symbol") %>
</span>
<input
id="currency-amount"
class="currency-input__input"
type="text"
inputmode="decimal"
value="1234.5"
aria-describedby="currency-amount-unit currency-amount-sr"
data-stimeo--currency-input-target="display"
data-action="input->stimeo--currency-input#onInput
blur->stimeo--currency-input#format" />
<span id="currency-amount-unit" class="currency-input__unit">
<%= t("components.currency_input.demo.unit") %>
</span>
</div>
<%# Channel that conveys the normalized actual value to assistive tech (visually hidden). %>
<span id="currency-amount-sr" class="visually-hidden"
data-stimeo--currency-input-target="srValue"></span>
<input type="hidden" name="amount" data-stimeo--currency-input-target="field" />
</div>
/*
* Presentation-only styles for the currency-input demo.
* The library handles the formatting logic; the input and currency-symbol look are
* defined here.
*/
.currency-input {
width: min(20rem, 100%);
}
.currency-input__label {
display: block;
margin-bottom: 0.375rem;
font-weight: 600;
}
.currency-input__field {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0 0.75rem;
border: 1px solid var(--border);
border-radius: 0.5rem;
background: var(--surface, var(--surface-card));
}
.currency-input__field:focus-within {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
.currency-input__symbol,
.currency-input__unit {
color: var(--text-muted, var(--color-text-muted));
}
.currency-input__input {
flex: 1 1 auto;
min-width: 0;
padding: 0.5rem 0;
border: 0;
background: transparent;
font: inherit;
text-align: right;
font-variant-numeric: tabular-nums;
}
.currency-input__input:focus {
outline: none;
}
// Demo that subscribes to the currency-input change event to inspect the normalized
// number that gets submitted. The display uses thousands separators, while the hidden
// input holds the unseparated number.
document.querySelectorAll('[data-controller~="stimeo--currency-input"]').forEach((root) => {
root.addEventListener('stimeo--currency-input:change', (e) => {
// In real use, e.detail.value (the number) feeds form submission or aggregation.
console.log('currency value:', e.detail.value, 'formatted:', e.detail.formatted);
});
});
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--currency-input"
Targets
| Name | Description | Attribute |
|---|---|---|
display
required
|
The visible text input that shows the grouped amount and stays the sole Tab stop. | data-stimeo--currency-input-target="display" |
field
required
|
Hidden input holding the machine-readable normalized number for form submission. | data-stimeo--currency-input-target="field" |
srValue
|
Visually-hidden span (referenced by aria-describedby) announcing the normalized amount to assistive tech. |
data-stimeo--currency-input-target="srValue" |
Values
| Name | Description | Attribute |
|---|---|---|
locale
|
BCP 47 locale used for grouping and decimal separators; malformed tags fall back to the default (default en-US). |
data-stimeo--currency-input-locale-value |
currency
|
ISO 4217 currency code; when set, the SR text is formatted as currency; malformed codes fall back to empty (default empty). | data-stimeo--currency-input-currency-value |
precision
|
Number of decimal places for the fixed-precision rounding on blur; out-of-range values fall back to the default (default 2). | data-stimeo--currency-input-precision-value |
Actions
| Name | Description | Action |
|---|---|---|
format
|
Applies the fixed-precision rounding on blur. | stimeo--currency-input#format |
onInput
|
Re-groups digits as the user types while preserving the caret position. | stimeo--currency-input#onInput |
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires when the committed value changes, including back to empty; detail { value: number | null, formatted: string } (null with an empty formatted on clear). |
stimeo--currency-input:change |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
value |
Display input | The grouped, human-readable string. |
value |
Hidden field | The normalized number for submission (no separators). |
text |
srValue span | The normalized value announced to assistive tech via aria-describedby. |
data-stimeo--currency-input-empty |
Root element | Present while the value is empty. |