NumberBox

The NumberBox lets users enter and adjust numbers (quantity, price, tax rate, font size…). Its style and behavior reproduce the WinUI 3 (Windows App SDK) NumberBox: it is essentially a TextBox + math parsing + NaN semantics, so this component embeds FluereInput (consistent with the embedded TextBox in the WinUI template) and implements WinUI's value-handling semantics, validation modes, spin buttons and inline expressions.

Values follow WinUI's Value ↔ Text dual channel: when the value changes, the formatted text is written back to the input box; on blur or Enter, the text is parsed and the value updated. Clearing the input box yields null (corresponding to WinUI's NaN), at which point placeholder is shown.

Difference from the original plan in docs/todo.md: reuse of reka-ui's NumberField was originally planned, but its NumberFieldInput has a few hard conflicts with WinUI — Home / End are mapped to min / max (WinUI leaves them to the TextBox to move the caret), invalid input clears the value on blur (WinUI keeps the original value), arrow keys snap to the step (WinUI adds directly), the wheel direction is reversed, and LargeChange is hardcoded to 10 steps. Here the "reuse" approach is kept but re-implemented as a self-contained implementation: the format / parse / expression / step four pure-function layers plus the use-number-box / use-spin-repeat two hooks, each unit-testable independently.

Basic usage

v-model binds the numeric value, header is WinUI's title row (also the default accessible name), and description renders below the control and acts as aria-describedby.

QuantityUnit priceSupports decimals: formatted to the step's decimal places on blur or Enter

Quantity: 12 · Unit price: 199.9 (clearing the input yields null, corresponding to WinUI's NaN)

Basic usage

Range and step

min / max map to Minimum / Maximum (by default no bounds are set, in which case aria-valuemin / aria-valuemax are not rendered); step is SmallChange (arrow keys / wheel / spin buttons), largeStep is LargeChange (PageUp / PageDown). Consistent with WinUI: no step snapping, values are only clamped (or wrapped) when out of range.

Quantity (0–500)Ratio (0–1, step 0.1)

Arrow keys / wheel move by step, PageUp / PageDown by largeStep; out-of-range values are clamped to the range, and — as with WinUI — there is no step snapping.

Range and step

Pass wrap-enabled for IsWrapEnabled: after reaching an endpoint the value wraps around to the other end, and both buttons stay usable.

Spin buttons

spinButtonPlacementMode maps to SpinButtonPlacementMode: hidden (default) / inline (up/down buttons inline to the right of the input box) / compact (a button panel floats up on focus). inline shares a TextBox border with the input box; the buttons are 32×24, margin 4, corner radius 4; holding a button steps once immediately, then repeats every 250ms (the WinUI RepeatButton Delay / Interval defaults). Buttons never enter the Tab sequence (WinUI IsTabStop=False) but can be activated with Enter / Space.

Font size (hold the button to keep stepping)Zoom

The inline buttons and the input box share one TextBox border: the buttons are 32×24 with margin 4 and corner radius 4, repeating every 250ms while held (the WinUI RepeatButton Delay / Interval defaults).

Inline buttons
OpacityGrid columns (up / down buttons appear on focus)

Compact mode shows up / down indicators to the right of the input box; on focus a 36×36 button panel floats out on the right (corner radius 8 + shadow depth 16) and collapses on blur.

Compact buttons

Inline expressions

accepts-expression maps to AcceptsExpression: on blur or Enter it evaluates expressions composed of + - * / ^ and parentheses, with precedence ^ > * / > + -. After evaluation the raw expression is not preserved (it is replaced by the result).

Computed valueBudget (you can enter 200*3+500)Tax rateEvaluated on blur or Enter, using parentheses and the precedence ^ > * / > + −

Computed value: 0 · Budget: 1000 · Tax rate: 0.06

Inline expressions

Validation modes

validationMode maps to ValidationMode:

  • invalidInputOverwritten (default): on blur / Enter, invalid input is overwritten by the formatted text of the current value, and out-of-range values are clamped back into the range;
  • disabled: keeps the user's input and out-of-range values, leaving validation to the consumer.

Escape always discards the input and returns to the current value; Enter commits the input (and swallows the key to avoid triggering an implicit form submission, matching how WinUI handles Enter).

Default: invalid input overwrittenTry typing abc — it returns to 20 on blur; type 150 — it is clamped to 100ValidationMode=Disabled: Keep inputThe text is kept as typed, while the value stays at the last successfully parsed result

Strict mode: 20 · Lenient mode: 20

Validation modes

Formatting and locale

formatOptions (Intl.NumberFormatOptions) maps to NumberFormatter, and locale determines the decimal point / group separators / minus sign — display and parsing share the same set of symbols. SSR apps are recommended to pass locale explicitly: by default the runtime's default locale is used, but the server and browser defaults may differ (e.g. the de-DE decimal point), producing a hydration mismatch on the first frame.

WinUI's default DecimalFormatter is set to IntegerDigits(1) + FractionDigits(0), which on several WinAppSDK versions rounds the displayed value to an integer (microsoft/microsoft-ui-xaml#8780: entering 25.8 shows 26 after blur). On the web side decimals are not dropped by default (maximumFractionDigits: 20), otherwise a usage such as step = 0.1 becomes unusable at the display layer; pass :format-options="{ maximumFractionDigits: 0 }" to get WinUI's integer look. Before display, a WinUI-style 10-significant-digit rounding is also applied to wipe out floating-point noise such as 0.1 + 0.2 = 0.30000000000000004.

Integer appearance (maximumFractionDigits: 0)25.8 displays as 26 — the same look as WinUI's default DecimalFormatter with FractionDigits(0)Fixed two decimal placesde-DE: 1.234,5The decimal point and group separators follow the locale, and parsing accepts the same set of symbols
Formatting and locale

Disabled

Disabled (with spin buttons)Disabled (no value)

When disabled, the input box falls to the TextControl…Disabled tier and the spin buttons move to …ForegroundDisabled; the header and description both gray out.

Disabled state

Form submission

When placed inside a <form> with a name, it adds a hidden native <input type="hidden"> submitting name=value (an empty string when there is no value).

Amount
Submitted amount: —
Form submission

API

Prop (Props)TypeDefaultDescription
modelValuenumber | null—Current value (v-model); null means no value (WinUI NaN)
defaultValuenumber | nullnullUncontrolled initial value
min / maxnumber— (no bounds)Range (WinUI Minimum / Maximum)
stepnumber1Step (WinUI SmallChange)
largeStepnumber10Large step (WinUI LargeChange, PageUp / PageDown)
disabledbooleanfalseWhether the control is disabled
headerstring—Top title (WinUI Header), also the default accessible name
labelstring—Accessible name (takes precedence over header)
descriptionstring—Description below the control (WinUI Description), also used as aria-describedby
placeholderstring—Placeholder (WinUI PlaceholderText), shown only when there is no value
namestring—Form submission name, paired with the hidden native input
localestring—BCP 47 language tag; determines the formatting and parsing symbols
formatOptionsIntl.NumberFormatOptionsmaximumFractionDigits: 20Number formatting (WinUI NumberFormatter)
spinButtonPlacementMode'hidden' | 'compact' | 'inline''hidden'Spin button presentation (WinUI SpinButtonPlacementMode)
validationMode'invalidInputOverwritten' | 'disabled''invalidInputOverwritten'Validation mode (WinUI ValidationMode)
wrapEnabledbooleanfalseWrap around at the endpoints (WinUI IsWrapEnabled)
acceptsExpressionbooleanfalseWhether inline expressions are parsed (WinUI AcceptsExpression)
increaseLabelstring'Increase'Accessible name of the plus button (localized from a resource in WinUI)
decreaseLabelstring'Decrease'Accessible name of the minus button
EventPayloadDescription
update:modelValuenumber | nullValue change (v-model)
valueChanged{ oldValue: number | null, newValue: number | null }Value change (corresponds to WinUI ValueChanged)
Slot (Slots)Description
headerTop title row (WinUI Header / HeaderTemplate)
descriptionDescription below the control (WinUI Description)

Keyboard and pointer handling follow NumberBox.cpp: arrow keys step by SmallChange, PageUp / PageDown by LargeChange, Enter commits, Escape reverts, and gaining focus selects the whole text; the mouse wheel only responds while the input box holds focus, scrolling up increments (MouseWheelDelta > 0). WinUI leaves Home / End to the TextBox, and this component likewise does not treat them as step keys.

Accessibility: the input box carries role="spinbutton" with aria-valuenow / aria-valuemin / aria-valuemax (the latter two are not rendered when no bounds are set, matching WinUI's behavior of composing them into the UIA name only once Minimum / Maximum are rewritten); the spin buttons do not enter the Tab sequence but can be activated by keyboard, and each carries an accessible name.

Two web-side approximations: the compact popup approximates WinUI's Acrylic surface with colorNeutralBackground1 + shadow16 (the Fluent token set has no material tokens); when the inline buttons appear, the right side of the input box gives up an extra 84px to keep text from pressing onto the buttons (WinUI lets text press under the buttons).

WinUI's Text property is not exposed as a separate prop: on the web side the input-box text is derived from v-model plus formatting (Value → Text); use formatOptions for custom text display. The WinUI initialization order of "when both Text and Value are set, Value wins" therefore does not apply.

v0.5.0-alpha.1