Skip to content
ShineUI
Install

InputNumber

Numbers that line up.

<sui-input-number>Text inputsStable

About

Numeric field with steppers, a money mode and prefix or suffix text. It groups and separates the way locale does and accepts what it displays. Form-associated: it posts the raw number under its name, never the formatted text, and binds to React, Vue (v-model) and Angular forms. Use it for amounts, quantities and rates; always give it a label.

Usage

<sui-input-number label="Quantity" name="quantity" min="0" max="99" value="3"></sui-input-number> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-input-number"); el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiInputNumber } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return <SuiInputNumber label="Quantity" name="quantity" min="0" max="99" value="3" onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiInputNumber } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiInputNumber label="Quantity" name="quantity" min="0" max="99" value="3" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiInputNumber } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiInputNumber], template: `<sui-input-number label="Quantity" name="quantity" min="0" max="99" value="3" (suiChange)="onChange($event)"></sui-input-number>`, }) export class Example { onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The inner input is a spinbutton: aria-valuenow, aria-valuemin and aria-valuemax follow the value and the bounds. A press on the prefix or the suffix focuses the field.

<div style="max-width:320px"> <sui-input-number label="Quantity" name="quantity" min="0" max="99" value="3" help="Whole units only."></sui-input-number> </div>

Prefix and suffix

prefix-text and suffix-text are plain text on either side of the number; a press on either focuses the field. money adds the $ prefix, two decimals and end-aligned tabular numerals, and owns the prefix spot.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-input-number label="Order" prefix-text="#" value="1043" hide-buttons></sui-input-number> <sui-input-number label="Quantity" min="0" value="3" suffix-text="units"></sui-input-number> <sui-input-number label="Distance" suffix-text="km" decimals="1" step="0.5" value="12.5"></sui-input-number> <sui-input-number label="Amount" money value="6420"></sui-input-number> </div>

Locale and currency

locale sets the grouping and decimal characters, and the field accepts what it shows. money with a currency code puts the symbol where the locale does and takes the fraction digits from the currency. A screen reader hears the text on the screen, through aria-valuetext: "$1,234.50", not the 1234.5 the field posts.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-input-number label="Germany" locale="de-DE" money currency="EUR" value="6420"></sui-input-number> <sui-input-number label="India" locale="en-IN" money currency="INR" value="1234567"></sui-input-number> <sui-input-number label="Japan" locale="ja-JP" money currency="JPY" value="6420"></sui-input-number> <sui-input-number label="France" locale="fr-FR" decimals="2" value="1234567.5" suffix-text="t"></sui-input-number> </div>

Decimals and step

decimals fixes the fraction digits on commit; step sets what the steppers and the arrow keys add. Neither changes what you may type.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-input-number label="Discount" suffix-text="%" min="0" max="100" decimals="1" step="0.5" value="7.5"></sui-input-number> <sui-input-number label="Unit weight" suffix-text="kg" decimals="3" step="0.25" value="12.125"></sui-input-number> </div>

Sizes

md is the default, one height with every select, picker and button. lg is the roomier field for a sign-in or marketing page; sm packs into table rows. The stepper column follows the height.

<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;align-items:end"> <sui-input-number label="Large" size="lg" value="42"></sui-input-number> <sui-input-number label="Medium" size="md" value="42"></sui-input-number> <sui-input-number label="Small" size="sm" value="42"></sui-input-number> </div>

In a row

A field with no width set fills its container, and in a flex row it takes the width its native input gives it, so it holds its own beside a button; a width on the host overrides it, and in a grid column it shrinks to the column.

<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end"> <sui-input-number label="Row" min="1" value="750"></sui-input-number> <sui-input-number label="Per page" min="10" max="100" step="10" value="50" style="width:120px"></sui-input-number> <sui-button variant="outline" size="md" label="Go to row"></sui-button> </div>

As a property

variant="quiet" draws the number as text in the surrounding font, its affixes beside it and no shell at rest: the fill and the steppers come on hover, the shell while the field has focus, since a focused field is being typed into. The label is not drawn but still names the field. inline is an alias; write quiet.

Estimate Seats Budget
<div style="display:grid;grid-template-columns:76px minmax(0,1fr);gap:12px 10px;align-items:center;max-width:320px;font:550 13.5px/1.5 var(--font-sans);color:var(--ink)"> <span style="font:460 12.5px/1.3 var(--font-sans);color:var(--ink-muted)">Estimate</span><sui-input-number variant="quiet" label="Estimate" size="sm" min="0" max="40" value="8" suffix-text="h" placeholder="None" hide-buttons></sui-input-number> <span style="font:460 12.5px/1.3 var(--font-sans);color:var(--ink-muted)">Seats</span><sui-input-number variant="quiet" label="Seats" size="sm" min="1" max="500" value="12"></sui-input-number> <span style="font:460 12.5px/1.3 var(--font-sans);color:var(--ink-muted)">Budget</span><sui-input-number variant="quiet" label="Budget" size="sm" money value="6420" hide-buttons></sui-input-number> </div>

States

A read-only field still posts its number but refuses the steppers and the arrow keys, since a step is an edit, and a disabled one posts nothing. hide-buttons drops the stepper column; the keys still work.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-input-number label="Credit limit" money value="0" error="Enter an amount above zero."></sui-input-number> <sui-input-number label="Approved amount" money value="6420" readonly help="Set when the invoice was approved."></sui-input-number> <sui-input-number label="Locked" value="480" disabled></sui-input-number> <sui-input-number label="Plain" value="42" hide-buttons></sui-input-number> </div>

In a form

The raw number posts under the host name, never the formatted text. A cleared field posts an empty string, a disabled one posts nothing, and Reset restores the number each field mounted with.

Submit to see what posts.
<form id="inum-form" style="display:grid;gap:14px;max-width:320px"> <sui-input-number label="Amount" name="amount" money value="1250.5"></sui-input-number> <sui-input-number label="Quantity" name="qty" min="1" max="99" value="2"></sui-input-number> <sui-input-number label="Approved amount" name="approved" money value="6420" readonly></sui-input-number> <sui-input-number label="Locked" name="locked" value="480" disabled></sui-input-number> <div style="display:flex;gap:8px"> <sui-button type="submit" variant="brand" label="Submit"></sui-button> <sui-button type="reset" variant="outline" label="Reset"></sui-button> </div> <output id="inum-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see what posts.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#inum-form'); const out = root.querySelector('#inum-out'); form.addEventListener('submit', (e) => { e.preventDefault(); out.textContent = [...new FormData(form)].map(([k, v]) => `${k}=${v || '(empty)'}`).join(' '); }); form.addEventListener('reset', () => { out.textContent = 'Reset. Submit to see what posts.'; }); }

Events

suiInput fires on every keystroke with the parsed number, or null while the text does not parse. suiChange fires on commit, at blur, Enter, an arrow key or a stepper, with the rounded and clamped number. suiFocus and suiBlur follow the input; the blur commit comes first.

Idle.
<div style="display:grid;gap:10px;max-width:320px"> <sui-input-number id="inum-events" label="Quantity" name="qty" min="0" max="50" value="8"></sui-input-number> <span id="inum-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#inum-log'); const el = root.querySelector('#inum-events'); el.addEventListener('suiInput', (e) => { log.textContent = `suiInput ${e.detail === null ? 'null' : e.detail}`; }); el.addEventListener('suiChange', (e) => { log.textContent = `suiChange ${e.detail === null ? 'null' : e.detail}`; }); el.addEventListener('suiFocus', () => { log.textContent = 'suiFocus'; }); el.addEventListener('suiBlur', () => { log.textContent = 'suiBlur'; }); }

Keyboard

KeyAction
0-9 comma point minusThe only characters the field accepts, plus the space a locale such as fr-FR groups with; every other printable key is dropped.
EnterCommits the typed number, rounded to decimals and clamped to min and max.
ArrowUp ArrowDownSteps the value by step and commits it.
PageUp PageDownSteps the value by ten times step and commits it.

API

<sui-input-number>

Properties

PropertyAttributeTypeDefaultDescription
autocompleteautocompletestringstring | undefinedNative autocomplete hint, e.g. "transaction-amount" or "postal-code".
currencycurrencystring'USD'The ISO 4217 code money formats in: USD by default, EUR, JPY. The symbol, its side and the fraction digits come from locale: "€" leads in English and trails in German, and the yen has no decimals. A code Intl does not know is shown as written, with two decimals. Ignored without money.
decimalsdecimalsnumbernumber | undefinedFraction digits. Defaults to the currency's own count in money mode (2 for dollars, 0 for yen), otherwise 0.
disableddisabledbooleanfalseDisables the field and its steppers.
errorerrorstringstring | undefinedError message; paints the field in the danger tone and sets aria-invalid.
helphelpstringstring | undefinedHelp text under the field. Replaced by error when set.
hideButtonshide-buttonsbooleanfalseHides the up/down stepper column. The arrow keys still step the number.
invalidinvalidbooleanfalseDanger border without a message (validation handled elsewhere).
labellabelstringstring | undefinedField label. Set one, or give the host an aria-label or aria-labelledby: without any the spinbutton has no name. A visible label wins over both host attributes.
localelocalestringstring | undefinedBCP 47 tag that sets the grouping and decimal characters, e.g. "de-DE" shows 1.234,50 and accepts it typed. Unset, the page's locale (setLocale() or <html lang>), else en-US, as on sui-date-picker. Typing works both ways: a period or a comma that is not the locale's decimal is read as a thousands separator.
maxmaxnumbernumber | undefinedUpper bound applied on commit.
minminnumbernumber | undefinedLower bound applied on commit.
moneymoneybooleanfalseMoney mode: the symbol of currency, its fraction digits and end-aligned tabular numerals. $1,234.50 with the defaults. The same pair as on the charts: money turns it on, currency names the money.
namenamestringstring | undefinedForm control name. Reflected: form entry construction reads the content attribute, while all three generated bindings set properties, so an unreflected name posts nothing at all.
placeholderplaceholderstringstring | undefinedPlaceholder text, shown when the field is empty.
prefixTextprefix-textstringstring | undefinedText before the number, e.g. "#" or "Approx.". Ignored in money mode, where the currency owns that spot. Named prefixText because prefix is a reserved member on every element.
readOnlyreadonlybooleanfalseDisplay only: the number can be selected and copied but not edited; the field still posts its value. The steppers and the arrow keys are off too, since a step is an edit.
requiredrequiredbooleanfalseMarks the field required (asterisk + valueMissing validity when empty).
sizesize"lg" | "md" | "sm"'md'Control height: sm 28 for table rows, md 34 for forms and toolbars (the default, shared with the select family and the buttons, so a form built from defaults is one height), lg 42 for a sign-in or marketing page. In the quiet variant it is the height of the box that paints on hover and while the field is in use; the number keeps the surrounding font.
stepstepnumber1Stepper and arrow-key increment.
suffixTextsuffix-textstringstring | undefinedUnit text after the number, e.g. "units" or "%". In money mode it follows a trailing currency symbol. Named suffixText to pair with prefixText.
tonetone"inverse" | undefinedInk for the quiet variant on the brand chrome. inverse takes the inverse inks, the inverse hover wash and the inverse focus colour, as sui-button's tone of the same name does, so the value reads on a tone="brand" toolbar. Opening the field shows its shell in the card's own inks. The default variant ignores it.
valuevaluenull | numbernullCommitted number. Mutable; null while the field is empty, which is how it starts, so a new record shows the placeholder and posts nothing it was not given. A commit happens on blur, Enter, an arrow key or a stepper, rounded to decimals and clamped to min and max.
variantvariant"default" | "quiet"'default'default draws the field shell. quiet draws the number as text in the surrounding font, with its prefix and suffix beside it and no border or fill at rest, for a record's properties column: the fill comes on hover, the steppers show on hover, and the shell while the field has focus, since a focused text field is one being typed into. The label is not drawn but still names the field, so the host prints the property name. Typing, the keyboard, events, validation and the form post are the same in both. Anything else falls back to default.

Events

EventDetailReactVueDescription
suiBlurvoidonSuiBlur@sui-blurFires when the input loses focus; no detail. Does not bubble.
suiChangenull | numberonSuiChange@sui-changeFires on commit (blur, Enter, arrows, stepper); detail is the committed number or null.
suiFocusvoidonSuiFocus@sui-focusFires when the input takes focus; no detail. Every text field carries the same pair, so a host arms a hint or a launcher the same way on each without listening for the native focusin. Does not bubble.
suiInputnull | numberonSuiInput@sui-inputFires on every keystroke; detail is the parsed, unclamped number, or null while the text does not parse: an empty field, or a lone - or ..

Methods

MethodSignatureDescription
select()select() => Promise<void>Selects the field text, so a retype replaces the value.
setFocus()setFocus() => Promise<void>Moves keyboard focus into the field.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also