Currency Input
stimeo--currency-input
An amount field that groups digits while keeping a clean number to submit.
The stimeo--currency-input controller groups digits (thousands separators) as you type and rounds to a fixed precision on blur, using Intl.NumberFormat for the configured locale. The grouped string stays in the visible field while a clean, separator-free number is kept in a hidden input for submission. Because a hidden input is not exposed to assistive tech, the normalized value is also mirrored to a visually-hidden span referenced by aria-describedby. The library provides behavior only — no styling and no validation.
<%# 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 (default en-US). |
data-stimeo--currency-input-locale-value |
currency
|
ISO 4217 currency code; when set, the SR text is formatted as currency (default empty). | data-stimeo--currency-input-currency-value |
precision
|
Number of decimal places for the fixed-precision rounding on blur (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; detail { value, formatted }. |
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. |