---
title: "Number input"
description: "The box of a number — typed in the locale's form or stepped by a key or a button, kept within limits, with its unit beside it — without a label row, help or error text of its own. For a number whose label stands elsewhere, or inside a field a product composes itself."
url: "https://tanrei.chnotchy.com/components/number-input/"
last_updated: 2026-10-23T21:17:12.000Z
---

# Number input

> The box of a number — typed in the locale's form or stepped by a key or a button, kept within limits, with its unit beside it — without a label row, help or error text of its own. For a number whose label stands elsewhere, or inside a field a product composes itself.

> [!NOTE]
> **Advanced**
>
> Most products place [Number field](https://tanrei.chnotchy.com/components/number-field/), which this component is a part of.

## Overview

A number input is the box of a [number field](https://tanrei.chnotchy.com/components/number-field/), alone: the container, the number, its prefix and its suffix, and the two steppers. It has no label row above it and no help or error text below it.

Use it bare where the label a person reads stands somewhere else:

| Where | The label a person reads | The input’s `label` |
| --- | --- | --- |
| Beside a slider, under one row title | The row’s title | The same words as the slider’s: “Scale” |
| A cell of a table | The column’s header | The column and the row: “Quantity, oak desk” |
| A size in a toolbar | The glyph or the text beside it | What it sets: “Font size” |

Bare, the input needs a `label` of its own. It is the accessible name. A stacked input never draws it; a filled or an outlined one draws it as the label that floats in the box.

It is an **advanced** component: the bare control, for a product that composes its own. Most forms want the [number field](https://tanrei.chnotchy.com/components/number-field/), which is this input inside a field, ready to use.

**Used in** the [number field](https://tanrei.chnotchy.com/components/number-field/). To frame it yourself, put it in a [field](https://tanrei.chnotchy.com/components/field/), which names it, describes it, marks it required and puts it in its error condition.

It is a block as wide as the place it is put in, bare and framed alike. It has no width of its own: for a narrower box, give it a narrower place.

On the web it is `<tanrei-number-input>`. The other platforms have not implemented it yet.

### Usage

`value` is a number, or `null` while the input holds none. It changes when the person **commits** — focus leaves, they press Enter, or they step — and not at every keystroke. `input` carries what the text reads as meanwhile.

**Web**

```html
<!-- Beside a slider, under a row title the product draws. Each is named in words. -->
<div class="row">
  <tanrei-slider id="scale" label="Scale" min="0.25" max="4" step="0.05" value="1.5"></tanrei-slider>
  <tanrei-number-input id="scale-number" label="Scale" min="0.25" max="4" step="0.05" value="1.5"
    suffix-text="×" value-text="1.5 times" align="end" no-steppers></tanrei-number-input>
</div>

<!-- A cell of a table: the column's header is the visible label. -->
<td><tanrei-number-input name="quantity-oak-desk" label="Quantity, oak desk" value="2" min="1" max="99"
  increment-label="Increase" decrement-label="Decrease"></tanrei-number-input></td>

<script type="module">
  import '@tanrei/web/components/number-input';
  import '@tanrei/web/components/slider';

  const slider = document.querySelector('#scale');
  const number = document.querySelector('#scale-number');
  // One value in two controls: each one's input is written to the other's value.
  slider.addEventListener('input', () => (number.value = slider.value));
  number.addEventListener('input', (event) => {
    if (event.detail.value !== null) slider.value = event.detail.value;
  });
  // A commit changed the value. It is already the new one: a number, or null.
  number.addEventListener('change', () => save(number.value));
</script>
```

**React**

```tsx
// An advanced component — a building block of others — is imported from the advanced entry.
import { NumberInput } from '@tanrei/react/advanced';

<NumberInput label="Scale" value={scale} min={0.25} max={4} step={0.05} suffixText="×"
  align="end" noSteppers onChange={(event) => setScale(event.currentTarget.value)} />
```

### With a slider

A slider is for a value chosen by feel, and a number input for one that is known. Put them in a row under a title of your own, give both the same name, limits and step, and write each one’s `input` into the other’s `value`, as the sample above does. Give `valueText` where the suffix would be read badly: “1.5 times” for `×`. `no-steppers` leaves out the two buttons the slider makes unnecessary; the arrow keys still step.

### In a field, by hand

A field around a number input is a number field written out. Leave the input’s own `label` empty: the field names it. `label`, `error`, `disabled` and the requirement are said on the field; `name`, `value`, the limits and the unit on the input.

```html
<tanrei-field label="Quantity" required required-text="Required"
  supporting-text="Up to 99 per order.">
  <tanrei-number-input name="quantity" min="1" max="99" invalid-text="Enter a number."
    increment-label="Increase" decrement-label="Decrease"></tanrei-number-input>
</tanrei-field>
```

The input tells its field what only it knows. The field’s help is aligned with the text in the box: it starts and ends at the box’s text padding, whether or not the steppers are drawn. A filled or an outlined input draws the field’s label itself, floating in the box, so the field draws no label row. A text that is not a number puts the field in error with the input’s `invalidText` — or the field’s own `errorText` where the input has none.

You keep nothing in step by hand: a number input has no character count.

## Specs

A number and its unit are one piece of writing. The prefix is written before the number and the suffix after it, in the direction of the unit’s own script, whatever the direction of the page. So “44 px”, “$ 120 USD” and “40 %” read in an Arabic or a Hebrew page as they do in an English one, and a unit written in Arabic or Hebrew letters stands at the number’s left. The steppers and the label follow the page. `align="end"` puts the number against its unit: at the right for a unit in Latin letters or signs, in both directions. For a Latin currency code at the number’s left, begin the suffix with a right-to-left mark (U+200F).

### What is the input’s, and what is the field’s

| Aspect | The number input | The field around it |
| --- | --- | --- |
| Draws | The box, the number, the placeholder, the prefix and the suffix, the two steppers, and the label that floats in a filled or an outlined box | The label row above a stacked input, the required or optional mark, the help, the error text |
| Holds | The value, the limits and the step, its place in a form, the events | Nothing |
| Says to assistive technology | A spin button: its value, its limits, its value text | The name, the help or the error text, required, invalid, disabled |

The three variants and the three sizes, `small` (32), `medium` (40) and `large` (56), are the text input’s, and the box is drawn and measured as the [text input](https://tanrei.chnotchy.com/components/text-input/) of the same variant and size. The default system ships `stacked` only.

The two steppers are text Buttons of the medium size, side by side. Under the default system they take 88 of the box’s width, and each is a full touch target. They are never drawn smaller and never stacked: where a row cannot afford them, turn them off. The keys still step. They are drawn from the Button’s own tokens, and are never in error.

What happens in the box — [the value and the text](https://tanrei.chnotchy.com/components/number-field/#the-value-and-the-text), how a text is read in a locale, and [stepping](https://tanrei.chnotchy.com/components/number-field/#stepping) — is the same bare, framed and in a number field, and is described once, on the number field’s page.

### Bare, without a field

- **The name** is `label`. `labelHidden` stops a filled or an outlined input from drawing it.
- **An error** is `error`, which you set: the box is drawn in its error condition and exposed as invalid. The input has no error text. Say what is wrong in words beside it, or frame it.
- **A text that is not a number** is kept, empties the value, draws the box in error and emits `entryError` with `invalidText`. `invalidText` is added to the input’s description; nothing draws it.
- **Required** is a field’s to say. A bare input has no requirement setting: where the form needs an answer, validate it yourself.

- Category: Text inputs
- Level: base
- Listing: advanced
- Used in: Number field
- Page: https://tanrei.chnotchy.com/components/number-input/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/number-input.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | Yes | Variants left out: `filled`, `outlined`. |
| Cupertino (Apple HIG-inspired) (`cupertino`) | Yes | — |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | — |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | — |

### Platforms

- Web: the element `<tanrei-number-input>`, registered by importing `@tanrei/web` (or only its own module, `@tanrei/web/components/number-input`).
- React: `NumberInput` from `@tanrei/react/advanced`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-number-input` | lagging |
| SwiftUI | `TanreiNumberInput` | planned |
| Jetpack Compose | `TanreiNumberInput` | planned |
| Flutter | `TanreiNumberInput` | planned |
| React Native | `NumberInput` | planned |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `label` (prop) | `label` | `label` | `label` | `labelText` | `label` |
| `labelHidden` (prop) | `labelHidden` | `labelsHidden` | `labelHidden` | `labelHidden` | `labelHidden` |
| `placeholder` (prop) | `placeholder` | `prompt` | `placeholder` | `hintText` | `placeholder` |
| `error` (prop) | `error` | `error` | `isError` | `isError` | `error` |
| `disabled` (prop) | `disabled` | `disabled` | `enabled` | `enabled` | `disabled` |
| `min` (prop) | `min` | `in` | `min` | `min` | `min` |
| `max` (prop) | `max` | `in` | `max` | `max` | `max` |
| `steppers` (prop) | `noSteppers` | `steppers` | `steppers` | `steppers` | `steppers` |
| `incrementLabel` (prop) | `incrementLabel` | `incrementLabel` | `incrementContentDescription` | `incrementSemanticLabel` | `incrementAccessibilityLabel` |
| `decrementLabel` (prop) | `decrementLabel` | `decrementLabel` | `decrementContentDescription` | `decrementSemanticLabel` | `decrementAccessibilityLabel` |
| `prefixText` (prop) | `prefixText` | `prefix` | `prefix` | `prefixText` | `prefix` |
| `suffixText` (prop) | `suffixText` | `suffix` | `suffix` | `suffixText` | `suffix` |
| `valueText` (prop) | `valueText` | `valueText` | `stateDescription` | `semanticValue` | `valueText` |
| `input` (event) | `input` | `onInput` | `onInput` | `onInput` | `onInput` |
| `change` (event) | `change` | `value` | `onValueChange` | `onChanged` | `onValueChange` |
| `action` (event) | `action` | `onSubmit` | `keyboardActions` | `onSubmitted` | `onSubmitEditing` |
| `entryError` (event) | `entryerror` | `onEntryError` | `onEntryError` | `onEntryError` | `onEntryError` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

- `container`: The input's box: it holds the prefix, the input, the suffix and the steppers on one line. It measures and is drawn as the text input's container of the same variant and size, carries the hover state layer and shows focus by its edge.
- `outline` (optional): The border of an outlined number input, which opens a gap where the floated label crosses it; in a stacked one, the line drawn inside the container's edge while it is focused or in error, which takes no layout space.
- `active-indicator` (optional): The line along the bottom of a filled number input; its focused state is thicker and colored.
- `label` (optional): In a filled or outlined input, its name drawn inside the box: it rests in the input line while the input is empty and not focused, and floats otherwise. A stacked input draws none: the label above it is the row of the Field that frames it.
- `required-asterisk` (optional): After the floating label's text, an asterisk, where the Field that frames the input says the control is required and marks it (NMI-7). Kept from assistive technology.
- `input`: The number as text, in the platform's native text element: what the person typed while they edit, the committed number in the locale's form otherwise. One line, drawn with tabular figures.
- `placeholder` (optional): A hint shown while the input is empty; never a label and never a value.
- `prefix` (optional): Fixed text before the number that is part of the field and not of the value: a currency sign.
- `suffix` (optional): Fixed text after the number that is part of the field and not of the value: the unit (px, %, kg). It is read with the value.
- `stepper` (optional): One of two buttons at the end of the container that step the number down and up by step: icon-only Buttons of the text variant showing remove and add, drawn from Button's tokens. Not focus stops; the arrow keys do their work.
- `caret`: The text cursor, colored by the caret part of the variant.

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `variant` | `enum`: `stacked`, `filled`, `outlined` | `"stacked"` | `variant` | The text input's three looks, so that a number input is drawn as the text inputs beside it: stacked is a filled box that shows focus and error with a line inside its edge and draws no label — the label of the Field that frames it stands above it; filled and outlined float the label inside the box. Use one style per form. A design system may leave some out (the Tanrei system ships stacked only) or add values. |
| `size` | `enum`: `small`, `medium`, `large` | `"medium"` | `size` | Minimum height of the container: stacked 32 / 40 / 56, as tall as a Button of the same size, so that the box and a Button side by side in one row stand as tall as each other; filled and outlined 40 / 48 / 56, which hold the label that floats inside them. These are the text input's three sizes, small for a bar, a toolbar or a dense row of controls. The hit area always meets the minimum target. A design system may add values. |
| `value` | `number` | — | `value` | The committed number, or null (the platform's absent value) while the field holds none. It changes when the person commits — focus leaves, the action key, a step — not on every keystroke; input carries what is being typed. |
| `label` | `string` | `""` | `label` | Names the input for assistive technology where no Field frames it — a number under a column's head, in a toolbar, beside a slider under one row title; a filled or outlined input also draws it, as the label that floats in its box. Inside a Field leave it empty: the Field's label names the input, and is what floats. |
| `labelHidden` | `boolean` | `false` | `label-hidden` | Does not draw the floating label of a filled or outlined input, which stays the accessible name. Inside a Field, hide the label on the Field. A stacked input draws no label, so this changes nothing there. |
| `placeholder` | `string` | `""` | `placeholder` | A hint shown while the input is empty and the label is not in the input line. Never a replacement for a name. |
| `error` | `boolean` | `false` | `error` | Draws the input in its error condition and exposes it as invalid. A Field sets it from its own error; set it here only where no Field frames the input. What a value means is the product's to validate; the input never sets this itself. A number input also enters an error of its own while the text it was left with is not a number (NMI-21). |
| `disabled` | `boolean` | `false` | `disabled` | Prevents focus, editing, stepping and form submission. Inside a Field, disable the Field, so that its label follows. |
| `readOnly` | `boolean` | `false` | `readonly` | The number can be focused, selected and copied, but not changed: no key steps it and no stepper is drawn. |
| `min` | `number` | — | `min` | The lowest number a commit keeps; a lower one is raised to it. Null for no limit. It is also where the steps are counted from. |
| `max` | `number` | — | `max` | The highest number a commit keeps; a higher one is lowered to it. Null for no limit. |
| `step` | `number` | `1` | `step` | How far Up Arrow, Down Arrow and a stepper move the number; Page Up and Page Down move ten times as far. It does not limit what may be typed. |
| `steppers` | `boolean` | `true` | — | Draws the two stepper buttons at the end of the field. False for a field in a row too narrow for them, or beside a slider that does their work; the keys still step. |
| `incrementLabel` | `string` | `""` | `increment-label` | The accessible name of the stepper that steps up, in the product's language ("Increase"). The product's: Tanrei has no word for it. Empty reads the product's words. Required while the steppers are shown. |
| `decrementLabel` | `string` | `""` | `decrement-label` | The accessible name of the stepper that steps down, in the product's language ("Decrease"). The product's: Tanrei has no word for it. Empty reads the product's words. Required while the steppers are shown. |
| `prefixText` | `string` | `""` | `prefix-text` | Fixed text before the number, not part of the value: a currency sign ("$", "¥"). |
| `suffixText` | `string` | `""` | `suffix-text` | Fixed text after the number, not part of the value: the unit ("px", "ms", "%", "kg"). Read to assistive technology with the number. |
| `valueText` | `string` | `""` | `value-text` | The value as words ("12 pixels", "3 stars"), given to assistive technology in place of the number with its prefix and suffix. The same text a Slider beside the field takes as its valueText. |
| `align` | `enum`: `start`, `end` | `"start"` | `align` | Where the number stands in the input: at the start, as the text of every other field, or at the end, for a number in a column of numbers whose digits should line up. |
| `minFractionDigits` | `number` | `0` | `min-fraction-digits` | The fewest digits shown after the decimal separator; zeros fill them (2 for an amount of money: "12.00"). |
| `maxFractionDigits` | `number` | `3` | `max-fraction-digits` | The most digits kept after the decimal separator: a commit rounds to it. 0 makes the field hold whole numbers and asks for a keyboard without a separator. |
| `locale` | `string` | `""` | `locale` | A BCP 47 language tag ("en-US", "de-DE", "ar-EG") for how the number is shown and read: its digits and its decimal separator. Empty uses the platform's current locale. The value itself is a number in every locale. |
| `invalidText` | `string` | `""` | `invalid-text` | The words for an input committed with a text that is not a number ("Enter a number"): given to the Field that frames it, which shows them as its error text and, while they are empty, shows its own errorText (NMI-21, FLD-28). |

### Events

| Event | Description |
| --- | --- |
| `input` | The person changed the text, or a step changed the value; carries the number the text reads as — not clamped, not rounded — or the empty value while it reads as none. Emitted for every change, not when the product sets value. What a Slider beside the field follows. |
| `change` | A commit changed the value: focus left the field, the action key was pressed, a key or a stepper stepped it, or a held stepper was released; carries the value. |
| `action` | The person pressed the keyboard's action key (Enter, Return, Done). |
| `entryError` | The person committed a text that is not a number; carries the reason (invalid) and invalidText, the words the input has for it. The entry error itself is shown by the Field that frames the input (NMI-21). |

### States

`enabled`, `disabled`, `hovered`, `focused`, `error`

### Accessibility

- Role: `spinbutton`
- Labeling: The accessible name is the label of the Field that frames the input, or its own label where no Field does; a filled or outlined input draws that name floating, and labelHidden only stops the drawing. The placeholder is never the name. The description is what the Field gives: its supporting text with, in error, the error text after it and its optional text; an unframed input in an entry error is described by invalidText. The input is exposed as invalid while error is true, its Field is in error or an entry error lasts; as required while its Field says so, whichever mark is drawn; and as read-only while readOnly is true. It is a spin button whose value is the committed number, whose limits are min and max, and whose value text is valueText or the number with its prefix and suffix. The steppers are buttons named by decrementLabel and incrementLabel; they are not in the focus order.
- Minimum target size: yes

| Keys | Action |
| --- | --- |
| Tab / Shift+Tab | Moves focus to the input, which is the field's one stop, and on from it. The steppers are not stops. |
| Typing, Backspace, Delete, Left Arrow, Right Arrow, Home, End, selection and clipboard shortcuts | Edit the text as the platform's own text element does. Nothing typed is refused, clamped or rounded until the field is committed. |
| Up Arrow / Down Arrow | Step the number up or down by step, to the next number on the step's grid, within min and max (NMI-22, NMI-23). |
| Page Up / Page Down | Step by ten times step. |
| Enter | Commits the text — clamps, rounds, or reports a text that is not a number — emits action, and submits the enclosing form on the web. |

- A number input is for a number the product reads as a number: a quantity, a size, a percentage, an amount. Digits that are not a quantity — a postal code, a card number, a telephone number, a one-time code — are text: use a text input with the matching type, which never steps, clamps or reformats them.
- Text entry happens in the platform's native text element: on the web an \<input type="text"> with an inputmode in the shadow root, carrying the spinbutton role and its value attributes — not the browser's number input, whose spinner cannot be drawn from tokens, whose value is emptied by a text it cannot read, whose parsing ignores the product's locale, and which in some browsers changes its value when the wheel turns over it.
- The steppers are out of the focus order on purpose (NMI-25): the arrow keys step from the input, as in every spin button, and a form of twenty numbers stays twenty stops long. They stay in the accessibility tree, named, because a person exploring a touch screen with a screen reader has no arrow keys.
- Two steppers side by side take 88 of the field's width under the default system, each a full target. That is the honest size: a pair stacked inside a 48 field would be two 24-high targets that cannot grow over each other. Where a row cannot afford 88, set steppers to false; the keys, the assistive-technology actions and a Slider beside the field still step.
- The wheel never changes the number (NMI-24). A number that changes because the page was scrolled past it is a wrong number nobody typed.
- Limits are applied when the field commits, never while a person types (NMI-20): a text on its way to a valid number must be allowed to pass through invalid ones.
- Errors are words (WCAG 3.3.1, 1.4.1): a text that is not a number is kept, marked, and described by invalidText; nothing is silently thrown away or silently replaced by the last good number.
- The unit is read with the value (NMI-34). Where a suffix would be read badly ("×", "px"), give valueText in words.
- Web: a touch browser zooms the page into an input whose text is smaller than 16 px. Both sizes use 16 px text (comp.input-box.input.typescale.medium); a layer that sets a smaller style brings the zoom back.
- Heights are minimums: at 200% text size the input grows, a floating label wraps rather than clips, and the number keeps its line.
- The input has no label row and no message of its own: a Field frames it and draws them (FLD-2 to FLD-11, FLD-25, FLD-26, FLD-28), or the product names it with label where its visible label is elsewhere. Unframed and unnamed, it is reported (NMI-12). Beside a slider under one row title, the slider and the number input are each named in words, given the same limits and step, and each one's input is written to the other's value.
- How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.
- A number and its affixes are spoken in the order they are written — prefix, number, suffix (NMI-34) — whatever side each is drawn on: the run's direction (NMI-39) changes where the unit stands, never what is said or what a typed or pasted text is read as (NMI-18).

### Behaviour

Requirements in EARS form, each with its stable id.

- **NMI-1** THE number input SHALL draw its container at least comp.input-box.stacked.container.height.medium tall WHILE variant is stacked (or a variant a design system adds, which is drawn as a stacked box) and at least comp.input-box.container.height.medium tall WHILE variant is filled or outlined, for size medium (and so on for each size), with a stacked or outlined number input's four corners and a filled one's top corners rounded by comp.input-box.container.shape, a filled one's bottom corners by comp.input-box.container.shape-bottom, all eased by sys.shape.corner-smoothing, and SHALL lay out in it one row, centred vertically: the input line — the prefix where there is one, the input taking the remaining width and the suffix where there is one (NMI-28), in the order and the direction NMI-39 gives — and at the container's end the two steppers WHILE they are shown (NMI-25), comp.input-box.container.gap.medium after the input line (and so on). Text SHALL be comp.input-box.container.padding-inline.medium from the container's edge and the input line SHALL have at least comp.input-box.stacked.container.padding-block.medium above and below it in a stacked box and comp.input-box.container.padding-block.medium in a filled or an outlined one (and so on for each size).
- **NMI-2** THE number input SHALL measure as the text input of the same variant and size measures (TXI-16): the same container height, paddings and corners, and the same text style and start edge for its input text, so that a number input and a text input side by side share every horizontal line of their boxes and a number input below a text input shares its start edges. THE number input SHALL be drawn, in every state, plain and in error, with the colors the text input of the same variant has for the parts of the same name. The two are drawn from one set of tokens (NMI-47): what a layer changes of the box, it changes for both. The rows around the two boxes — the label, the help, the error — are alike for another reason: they are the one Field's (FLD-1), whichever control it frames.
- **NMI-7** WHERE variant is filled or outlined, WHILE the floating label is drawn (NMI-8) and the Field that frames the input gives it an asterisk as its required mark (FLD-25) THE number input SHALL draw the character "*" after the label's text, comp.input-box.requirement.asterisk-gap from it, in the label's text style and at the label's place, resting and floated with it, colored by its required-asterisk part (comp.input-box.outlined.required-asterisk.color for an outlined input; its error. part WHILE the input is in error), and SHALL keep it from assistive technology, because the required state already says it (NMI-12). It SHALL draw no tag and no optional label, as the text input of that variant draws none (TXI-50): which mark a required control gets is the Field's to say (FLD-6), and a tag named there reaches a floating label as the asterisk. WHILE no Field frames it, or the Field gives no mark, or the label is not drawn, THE number input SHALL draw no mark.
- **NMI-8** WHERE variant is filled or outlined and the label is drawn THE number input SHALL rest the label in the input line, in comp.input-box.label.typescale.medium for size medium (and so on), WHILE the input is empty and not focused, and otherwise float it as the text input of that variant floats its label (TXI-13): to the top of a filled container, above the input line, or onto an outlined container's top edge with a gap of comp.input-box.outline.notch-padding in the outline on each side of it, in comp.input-box.label.floated-typescale.medium for size medium (and so on); a filled number input that draws its label SHALL be at least twice comp.input-box.container.padding-block-with-label.medium plus one line of each of those two text styles tall (and so on for each size), so that it does not change height when the label floats. WHEN the label floats or rests THE number input SHALL move and resize it over comp.input-box.motion.label-duration with comp.input-box.motion.label-easing, and at once where motion is reduced. The label SHALL wrap rather than be cut and SHALL never be drawn over the input text. WHILE the input is empty THE number input SHALL show placeholder, in the placeholder color of its variant (comp.input-box.stacked.placeholder.color), only while the label is not in the input line — a stacked input, whose label is the row of its Field; floated; or not drawn — so that the two never overlap. The label it floats SHALL be its accessible name (NMI-12): the label of the Field that frames it, or its own label where none does. WHILE labelHidden is true — its own, or that Field's (FLD-25) — or it has no name THE number input SHALL draw no label. WHERE variant is stacked, or a variant laid out as stacked (NMI-43), THE number input SHALL draw no label at all: the label above it is the Field's row (FLD-2).
- **NMI-10** WHILE error is true — set by the product, or by the Field that frames it (FLD-8) — or the entry error of NMI-21 lasts THE number input SHALL draw every part of its container, in every state, from the error parts of its variant (comp.input-box.stacked.error.outline.color and the other error. parts), so that the box itself is marked and not only a text below it: a stacked number input draws, at rest, a line of comp.input-box.outline.width in comp.input-box.stacked.error.outline.color inside its container's edge, following its corners and taking no layout space; and it SHALL expose itself as invalid to assistive technology. The steppers SHALL stay as they are drawn: they are not in error. THE number input SHALL never set error itself — what a value means is the product's to validate — and SHALL show no error text of its own: the words that say what is wrong are the errorText of the Field that frames it, which that Field draws and gives the input as its description (FLD-7, FLD-11).
- **NMI-12** THE number input SHALL take its name, its description and its required state from the Field that frames it, as every control a Field frames does (FLD-10, FLD-11): the Field's label is its accessible name, whether or not any label is drawn, and the Field's supporting text with, in error, the error text after it its description. WHILE label is not empty THE number input SHALL use it as the accessible name instead, for an input no Field frames or one whose visible label is elsewhere — the head of a table's column, a toolbar, the title of a row that also holds a slider. It SHALL never use placeholder as the name, WHILE it has either name THE number input SHALL expose that one alone, over a name the product gives it on the element (aria-label or aria-labelledby); and WHILE it has neither name and the product gives it a name of its own on the element — aria-label, or aria-labelledby naming an element — THE number input SHALL expose that name as its accessible name; IF it has none of the three it SHALL still render and work, and in debug builds SHALL report that it has no accessible name. WHILE its Field says it is required THE number input SHALL expose the required state to assistive technology (and, on the web, a missing value to the form's constraint validation, NMI-35), whichever mark is drawn and where none is; it has no requirement setting of its own and SHALL NOT validate because of one.
- **NMI-14** WHEN the user presses a floating label, the prefix, the suffix or the container's padding THE number input SHALL move focus to its input and do nothing else: it SHALL NOT change the number or select it, and SHALL draw no press answer, because a press on a field is not an action. A press on the label row of the Field that frames it focuses the input too, and a press on that Field's supporting row does nothing: both are the Field's (FLD-12). WHERE the container is shorter than the minimum target THE number input SHALL extend its hit area vertically to sys.target.touch for touch input and sys.target.pointer for fine pointers, without changing its visual size; a platform that cannot tell the input type SHALL use sys.target.touch. THE number input SHALL add one stop to the focus order, its input, and no other: the steppers are not focus stops (NMI-25).
- **NMI-15** WHILE hovered or focused THE number input SHALL render its container, active indicator, outline, floating label and its asterisk, input, placeholder, prefix and suffix, caret and opacity, and the widths of its outline and active indicator, from the component tokens of the highest-precedence active state, with precedence focused over hovered (their error parts WHILE error is true); unlike a button, focus gained by pointer or touch SHALL select the focused state, because a field shows where typing will go however it was focused. THE number input SHALL show focus by its focused tokens and SHALL NOT draw the focus ring: the focused active indicator or outline is its focus indicator, and a stacked number input draws it as a line of its focused outline width, in the focused outline color of its variant, inside the container's edge, following its corners and taking no layout space, so that nothing in the container moves as the line appears, thickens or goes; WHERE sys.behavior.focus-style is glow it SHALL also draw the glow around the container while focused. A hovered stacked number input SHALL show the state layer on its container and draw no edge. Selected text in the input SHALL be highlighted in comp.input-box.selection.color and drawn in comp.input-box.selection.content.color in every variant and state. WHEN its state, error or disabled changes THE number input SHALL change its colors and widths over the state duration of its size (comp.input-box.motion.state-duration.small, .medium or .large) with comp.input-box.motion.state-easing, and at once WHERE motion is reduced.
- **NMI-16** THE number input SHALL hold as its value a number or nothing: value is a number, or empty (null, or the platform's own absent value) WHILE the field holds none. It SHALL edit that number as text in the platform's native text element, on one line, and WHILE the person is editing the text is theirs: THE number input SHALL NOT rewrite, reformat, clamp, round or refuse what is typed, pasted or dictated, SHALL NOT move the caret, and SHALL follow the text input's rule for an input method (TXI-4): it leaves a composition to the native element and reads the text only when the composition ends. It SHALL give the input a numeric keyboard: with a decimal separator WHILE maxFractionDigits is greater than 0, and with a minus sign WHILE min is empty or below 0, where the platform's keyboards can say so; without autocapitalization, autocorrection or suggestions.
- **NMI-17** WHEN a value is committed (NMI-20) or the product sets value (NMI-31) THE number input SHALL write the number into its input in the form of locale — a BCP 47 language tag, or WHILE locale is empty the platform's current locale: that locale's digits and decimal separator, a minus sign before a negative number, at least minFractionDigits and at most maxFractionDigits digits after the separator with no other trailing zeros, no exponent and no grouping separators, so that what is shown is what can be typed back. An empty value SHALL be an empty input, which shows placeholder as NMI-8 says. THE number input SHALL draw the number on one line in comp.input-box.input.typescale.medium for size medium (and so on) and the input color of its variant (comp.input-box.stacked.input.color), scrolling it sideways inside the input when it is longer than the input, and SHALL NOT cut it with an ellipsis.
- **NMI-18** THE number input SHALL read a text as a number leniently, by the locale's form: ignoring spaces around it and inside it; taking a hyphen-minus or a minus sign before the digits, or a plus sign, as its sign; taking ASCII digits, the locale's own digits and full-width digits as digits; taking the locale's decimal separator, once, as the separator; ignoring the locale's grouping separator wherever it stands between two digits; and ignoring prefixText before the number and suffixText after it WHERE they were typed or pasted with it ("12 px", "$5"). An empty text, or one of spaces only, SHALL read as no value. Any other text — letters, an exponent, two separators, a sign alone — SHALL read as not a number. THE number input SHALL NOT guess between two locales' forms: a character that is another locale's decimal separator and this locale's grouping separator is this locale's.
- **NMI-19** WHEN the person changes the text — by typing, deleting, pasting, cutting, dictating, autofill or an input method committing text — THE number input SHALL emit input once per change with the number the text now reads as (NMI-18), not clamped and not rounded, or with the empty value WHILE the text reads as no value or as not a number; WHEN a step changes the value (NMI-22) it SHALL emit input once with the new value. WHEN the product sets value THE number input SHALL NOT emit input or change. THE number input SHALL NOT change value itself while the person types: value is the committed number until the next commit.
- **NMI-20** WHEN the input loses focus, or the person presses the keyboard's action key (Enter, Return, Done), THE number input SHALL commit its text: WHILE the text reads as a number it SHALL take that number, raise it to min WHILE min is not empty and the number is below it, lower it to max WHILE max is not empty and the number is above it, round it to maxFractionDigits digits after the separator (a half away from zero), make it the value, and write it back into the input as NMI-17 says; WHILE the text reads as no value it SHALL make the value empty; WHILE the text reads as not a number it SHALL enter the entry error of NMI-21. It SHALL then emit change once with the value WHILE the value differs from the one last committed (the value when the input gained focus, or at the last change). THE number input SHALL clamp and round only when it commits, never while the person types, so that "25" can be typed on the way to "2.5" and "1" on the way to "100" above a minimum of 50; and it SHALL NOT move a committed number to a multiple of step: step says how far a stepper moves, not which numbers may be typed. WHEN the person presses the action key THE number input SHALL emit action after any change it causes, and on the web, inside a form, SHALL then request the form's submission as a native input does unless action was cancelled. The submission SHALL be requested through the form's default button (BTN-30): WHILE the form has a submit button — the platform's own or a Button of type submit — the first of them in document order SHALL be pressed, so that its click is emitted and its name and value are sent, and WHILE that button is disabled nothing SHALL be submitted; only a form with no submit button is asked directly.
- **NMI-21** WHEN the number input commits a text that reads as not a number (NMI-18, NMI-20) THE number input SHALL keep the text as the person left it, make the value empty, enter an entry error, and emit entryError once with the reason invalid and with invalidText, the words it has for the refusal. WHILE an entry error lasts THE number input SHALL be in the error condition as error puts it there (NMI-10) and SHALL report the refusal to the Field that frames it, with invalidText as its words, which that Field shows in its supporting row and announces (FLD-28: its errorText WHILE invalidText is empty; its help stays above either); WHILE no Field frames it invalidText SHALL be the input's description. THE number input SHALL end the entry error, without an event of its own, WHEN the person edits the text into a number or empties it, WHEN a step gives the field a value (NMI-22), or WHEN the product sets value, and SHALL then withdraw its report; the product's own error, and the error and the errorText of its Field, are untouched by it. THE number input SHALL NOT refuse characters as they are typed and SHALL NOT discard what it could not read, because a text that was put right is quicker than a text typed again.
- **NMI-22** WHEN the number input steps up or down by a count of steps — one for an arrow key and a stepper, ten for a page key (NMI-23, NMI-26) — THE number input SHALL start from the number its text reads as, or from the value WHILE the text reads as not a number, and move along the step's grid, the numbers that are min plus a whole multiple of step (zero plus a whole multiple WHILE min is empty): to the next number of the grid in that direction, so that a number between two of them goes to the nearer one in the direction of the step and not past it, and then one more number of the grid for each further step of the count; it SHALL then hold the result within min and max, compute it in decimal so that 0.1 stepped by 0.2 is 0.3, make it the value, write it into the input (NMI-17) with the caret at its end and all of it kept in view, and emit input and then change. WHILE the field holds no value a step in either direction SHALL give it the number nearest to zero that min and max allow and nothing more. A step that would not change the value — up at max, down at min — SHALL do nothing and emit nothing. WHILE step is 0 or less, or not a number, THE number input SHALL step by 1 and in debug builds report the step.
- **NMI-23** WHILE the input has focus, readOnly is false and no input method is composing, WHEN the person presses Up Arrow THE number input SHALL step up by step and WHEN the person presses Down Arrow step down by step (NMI-22); WHEN the person presses Page Up it SHALL step up ten steps and WHEN the person presses Page Down down ten steps: ten times step, on the grid of step and not on one of its own (NMI-22), so that with step 2 the value 42 goes to 62 and 82, as a slider's page keys (SLD-3) and the platform's own stepping of a number by ten go — a number input that stands beside a slider reaches the same numbers by the same keys. A key the platform repeats while it is held SHALL step once for each repeat. THE number input SHALL leave Home, End, Left Arrow and Right Arrow, and every arrow or page key pressed with a modifier key, to the text: they move the caret and select, as in any text input, and SHALL NOT change the value. THE number input SHALL have no key of its own for min and max.
- **NMI-24** THE number input SHALL NOT change its value WHEN the person turns a mouse wheel, scrolls a trackpad or swipes over it, whether or not it has focus — the page or the nearest scrolling ancestor scrolls instead — and SHALL NOT change its value by a drag along the field, its label or its unit. Only typing, the keys of NMI-23 and the steppers change the number.
- **NMI-25** WHILE steppers is true and readOnly is false THE number input SHALL draw two steppers at the end of its container, after the input line: first, in reading order, the one that steps down, showing the glyph remove of the system's icon set, then the one that steps up, showing the glyph add; each an icon-only Tanrei Button of the text variant and default width, at Button's small size WHILE size is small — a small box is as tall as a small Button, and a medium one would make it taller — and at Button's medium size otherwise, drawn from Button's own tokens, the two comp.number-input.stepper.gap apart, measured so that each keeps at least sys.target.touch along the row (two buttons narrower than that stand apart by what is missing), and the one nearest the end comp.number-input.stepper.inset from the container's end edge, both centred vertically in the container. Each SHALL be a Button in its press, its target and its states (BTN-5, BTN-8, BTN-9, BTN-10), with two differences: it SHALL NOT be a focus stop, because Up Arrow and Down Arrow do its work from the input and two more stops in every number of a form would cost more than they give; and it SHALL be exposed to assistive technology as a button named by decrementLabel or incrementLabel, so that a person who explores by touch can find and press it. THE number input SHALL disable the stepper that steps down WHILE the value is at or below min and the one that steps up WHILE it is at or above max, and both WHILE disabled is true. WHILE steppers is false or readOnly is true it SHALL draw neither and keep no room for them. THE number input SHALL NOT draw smaller steppers to save room, and SHALL NOT stack them: a product whose row cannot hold them sets steppers to false. WHILE incrementLabel or decrementLabel is empty THE number input SHALL still draw the stepper and in debug builds report that it has no accessible name.
- **NMI-26** WHEN the person presses a stepper by pointer or touch, or activates it through assistive technology, THE number input SHALL step once by step in that stepper's direction (NMI-22) when the press begins, and SHALL NOT move focus: a field that was not focused stays unfocused, so that a touch on a stepper does not raise the on-screen keyboard, and a focused input keeps its focus and its caret. A step from a stepper SHALL commit as every step does: it emits input and then change, and it ends an entry error.
- **NMI-27** WHILE a stepper is held down by pointer or touch THE number input SHALL repeat its step: after comp.number-input.motion.repeat-delay it SHALL step again, then again once every comp.number-input.motion.repeat-interval, and after its tenth repeated step once every comp.number-input.motion.repeat-interval-fast, until the press ends, the pointer leaves the stepper, or the value reaches min or max. A hold SHALL change how often the field steps and never how far: every step is one step. THE number input SHALL emit input for each step of a hold and change once, when the hold ends. These are waiting times, not motion: they SHALL be kept WHERE motion is reduced.
- **NMI-28** WHILE prefixText is not empty THE number input SHALL draw it before the number, comp.input-box.affix.gap from the input, and WHILE suffixText is not empty after the number, comp.input-box.affix.gap from the input — before and after along the direction of the number's run (NMI-39), not along the layout's — both on the input's line and in its text style, in the affix color of its variant (comp.input-box.stacked.affix.color; comp.input-box.stacked.error.affix.color WHILE error is true), exactly while the label is not in the input line (NMI-8). The prefix and the suffix are part of the field and never of the value: they cannot be edited or selected, a press on them focuses the input (NMI-14), and they SHALL NOT be cut — the input gives up its width first. THE number input SHALL read them to assistive technology with the number, as NMI-34 says, so that a unit or a currency is heard with the figure. THE number input SHALL have no control for choosing a unit: a unit the person can change is a select beside the field.
- **NMI-29** THE number input SHALL align the number in its input WHILE align is start to the field's start edge, as the text of every other field is — the right edge in a right-to-left layout, whatever the direction of the number's run (NMI-39) — and WHILE align is end to the end of the number's run, which is the edge of the input at which the suffix stands or would stand: for a number that stands against its unit and for a number in a column of numbers, because a number is written left to right and the digits of one place stand under each other only where the numbers end at one edge. The end of a left-to-right run is the right edge of the input in every layout direction, and the end of a right-to-left run the left edge: WHERE the run runs with the layout that is the field's end edge, and WHERE it runs against the layout — a number alone, or a number with a unit in Latin letters or signs, in a right-to-left layout — it is the field's start edge, so that there start and end are one edge. It SHALL draw the number, the placeholder, the prefix and the suffix with tabular figures where the typeface has them, so that the digits of two fields stand under each other and a number that is stepped does not move from side to side.
- **NMI-30** IF the product gives a min that is above max THE number input SHALL treat max as empty and in debug builds report it. IF the product sets a value below min or above max, or with more digits after the separator than maxFractionDigits, THE number input SHALL show that value as it is and SHALL NOT correct it: limits apply to what the person commits (NMI-20), and the product's own value is the product's to keep right. IF minFractionDigits is above maxFractionDigits THE number input SHALL use maxFractionDigits for both and in debug builds report it.
- **NMI-31** WHEN the product sets value THE number input SHALL write it into its input as NMI-17 says, replacing the text whatever the person had typed, end any entry error (NMI-21), and SHALL NOT emit input, change or entryError; a value set during an input method's composition SHALL be applied when the composition ends (TXI-4). WHEN the product changes min, max, step, minFractionDigits, maxFractionDigits or locale THE number input SHALL show its value again in the new form, enable and disable its steppers by the new limits (NMI-25), and SHALL NOT change the value or emit anything.
- **NMI-32** WHILE disabled is true — set by the product, or by the Field that frames it (FLD-14) — THE number input SHALL NOT be focusable or editable, SHALL NOT step by any input, SHALL NOT emit input, change, action or entryError, SHALL expose the disabled state to assistive technology, SHALL be left out of form submission on the web, SHALL disable both steppers, and SHALL render every part from the disabled component tokens of its variant (of its error parts WHILE error is true).
- **NMI-33** WHILE readOnly is true and disabled is false THE number input SHALL stay focusable, selectable and copyable, SHALL NOT let the person change the number by typing or by any key of NMI-23, SHALL draw no steppers (NMI-25), SHALL NOT emit input or change, SHALL expose the read-only state to assistive technology, SHALL be submitted with its form on the web, and SHALL render as an enabled field, including its hovered and focused states.
- **NMI-34** THE number input SHALL expose its input as a spin button (the web's spinbutton role; the platform's adjustable or stepper trait elsewhere) named as NMI-12 says: its value the committed number, its minimum min and its maximum max WHILE each is not empty, and its value text valueText WHILE that is not empty and otherwise the number as shown with prefixText before it and suffixText after it, each parted from it by a space ("$ 120 USD", "12 px", "40 %"); WHILE the field holds no value it SHALL expose no value and no value text. WHERE the platform's assistive technology can increase and decrease an adjustable element (a swipe up or down, an increment and a decrement action) those actions SHALL step by step as NMI-23 does. THE number input SHALL expose its steppers as buttons named by decrementLabel and incrementLabel with their disabled state (NMI-25), and SHALL keep the prefix and the suffix from being read a second time as texts of their own.
- **NMI-35** WHERE the platform has forms (the web) THE number input SHALL take part in them as a native input does: it takes name, submits its value as a decimal number with ASCII digits and a period, whatever the locale shows, or an empty string WHILE it holds no value, restores it on navigation, resets to the value it was first given, and reports to the form's constraint validation a missing value WHILE its Field says it is required (NMI-12) and the input holds none, and a bad input WHILE an entry error lasts (NMI-21).
- **NMI-36** THE number input SHALL be block-level, framed and bare alike: exactly as wide as the place it stands in — the width its layout gives it, which inside a Field is the Field's whole width (FLD-1), so that the box fills its Field — with no width of its own and never the platform's default width for a text element, as the text input is (TXI-63). It SHALL never be narrower than its paddings, its prefix and suffix and room for three digits together need, nor than its paddings and its steppers WHILE they are shown. WHILE the place it is given is narrower than the input's line — the paddings, the prefix, three digits and the suffix — and the steppers need beside each other, THE number input SHALL put the steppers on a row of their own below the input's line, inside the container: at its end, the one that steps down first as on one row, stepper.inset from the end edge and from the bottom edge, the input's line then taking the whole first row and ending at the text padding; and it SHALL put them back beside the line WHEN the two fit again. It SHALL never hide the steppers to fit, because a person without arrow keys, on touch, steps by them alone. A product that wants a narrower box gives it a narrower place.
- **NMI-37** WHILE the text size is enlarged THE number input SHALL grow with its texts: a floating label wraps and never clips, and the container grows with the input's line, keeping the height of its size as a minimum; the number stays on one line and scrolls inside its input, the prefix and the suffix grow with it and are never cut, and the steppers keep Button's sizes. At 200% no text SHALL be lost, lie over another part or need scrolling in two directions: where the input's line and the steppers no longer fit on one row, the steppers go to the row below it (NMI-36). The rows of the Field that frames it grow as FLD-16 says.
- **NMI-38** WHERE the platform draws with a person's own forced colors (the web's forced-colors mode, and a platform's equivalent where it has one) THE number input SHALL draw its number, its placeholder, its prefix and its suffix, and its floating label with its asterisk, in the platform's text color, SHALL draw an edge comp.input-box.outline.width wide in the platform's control text color around its container in every variant, and that edge in the platform's highlight color at the focused width while focused, and SHALL rely on no color of its own to mark an error: the error is carried by the invalid state (NMI-10) and by the words its Field shows (FLD-17). The steppers are Buttons and are drawn as Buttons are there; a disabled field and a disabled stepper take the platform's disabled text color.
- **NMI-39** THE number input SHALL treat its prefix, its number and its suffix as one run of writing — the prefix written before the number and the suffix after it (NMI-28) — and SHALL lay the three out in that order along the run's own direction, in every layout direction: an affix is part of what is written and not a control, so THE number input SHALL NOT place it by the layout direction as it places a stepper. The run's direction SHALL be the direction of the first strong character of prefixText and, WHERE prefixText has none, of suffixText — a letter, or a direction mark the product wrote there — and left-to-right WHERE neither has one (no affix, or signs alone: "$", "%", "×", "°"), because a number is written left to right; it is the direction the bidirectional algorithm gives the one text of prefix, number and suffix as a paragraph of its own. So a unit or a currency written in Latin letters or in signs stands in a right-to-left layout exactly as in a left-to-right one — "44 px", "$ 120 USD", "40 %", "1.5 ×", the number before its unit from left to right — and a unit written in a right-to-left script stands at the number's left, with a prefix of that script at its right, in a left-to-right layout too. THE number input SHALL decide the run's direction from prefixText and suffixText alone — never from the text being typed, from value or from locale — so that nothing changes sides while the person types. WHERE the layout direction is right-to-left THE number input SHALL mirror everything that is not the run: a floating label standing from the right edge with its asterisk to its left, and the steppers, which are controls, at the container's left end with the one that steps down to the right of the one that steps up, so that down is still the first of the two in reading order; all paddings applied by logical start and end, and the input line standing between the container's start padding and the steppers whatever its run's direction. THE number input SHALL NOT mirror the number: its digits, its sign and its separator SHALL be laid out left to right, and the number SHALL be aligned in its input as NMI-29 says.
- **NMI-40** THE number input SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the number input is shown THE number input SHALL draw itself for the new direction at once, without a motion of its own, keeping its value, its text, its caret and its focus.
- **NMI-41** WHERE the layout direction is right-to-left THE number input SHALL draw the glyphs of its steppers, remove and add, unmirrored: neither is marked as mirrored in the system's icon set, and the order NMI-39 gives the two steppers is the whole of their mirroring. THE number input SHALL NOT mirror a glyph itself, has no slot and draws no other glyph: the glyph before a label is the Field's, and the Field mirrors it (FLD-20).
- **NMI-42** THE number input SHALL lay out its floating label, its placeholder, its prefix and its suffix as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a unit and a currency code keep their own order in a right-to-left layout and reorder nothing beside them; THE number input SHALL still place its floating label and its placeholder, and align their lines, by the layout direction, not by the text's own; the prefix and the suffix it places by the direction of the number's run (NMI-39), which the same characters decide. The texts of the rows around it are laid out so by the Field that frames it (FLD-21).
- **NMI-43** WHEN variant or size names a value that the active design system does not have THE number input SHALL render with the default value's tokens, and in debug builds report the unknown value; WHEN variant names a value the active system has but the number input does not know by name (one a layer added) it SHALL draw no label of its own and draw its edge as a stacked number input does (NMI-15), with that value's own tokens; WHEN align names a value the number input does not know it SHALL behave as start and in debug builds report it. WHEN locale is not a language tag the platform knows THE number input SHALL use the platform's current locale and in debug builds report it.
- **NMI-44** THE number input SHALL have no words of its own: it SHALL take incrementLabel and decrementLabel from the instance's prop and, WHILE that is empty, from the product's words of the same name — the words a product sets once for all its components — and from nowhere else. IF neither gives incrementLabel or decrementLabel WHILE the steppers are drawn THE number input SHALL still draw both steppers and keep them working for a pointer, SHALL keep the one without a name from assistive technology — the spin button's own increase and decrease actions (NMI-34) do its work there, and a button without a name is worse than none — and in debug builds SHALL report the missing words once. The digits, the separator and the minus sign it shows are the platform's locale data and not the number input's words (NMI-17); invalidText is the instance's alone; and the words of a requirement are the Field's (FLD-24).
- **NMI-45** THE number input SHALL say to the Field that frames it what only the input knows, and SHALL say it again WHEN it changes: that it draws the label itself WHILE its variant floats one (NMI-8), so that the Field draws no label row (FLD-25); how far the line of its number stands from its own start edge and from its end edge — its inline padding at each (NMI-1), where the input line starts with whichever affix stands there (NMI-39), and, at the end, also WHILE the steppers are drawn, because a line of help is aligned with the text above it and not with a button — so that the Field's supporting row starts and ends there (FLD-26); and an entry error with its words (NMI-21, FLD-28). It keeps no count, so a number field fills its Field's count text with nothing (FLD-27). It SHALL take from that Field its name, its description and its required, invalid and disabled states (NMI-10, NMI-12, NMI-32), whether the label is hidden (NMI-8) and whether an asterisk follows it (NMI-7), so that a Field and a number input placed by hand are drawn, named, described and marked exactly as a number field is.
- **NMI-46** THE number input SHALL draw no label row above itself, no requirement tag, no help and no error text: those are the rows of the Field that frames it (FLD-2 to FLD-7). WHILE no Field frames it THE number input SHALL be complete as a bare control — its box, the label that floats in it where its variant floats one, the number, its unit and its steppers, its states and its events — and exactly as tall as its container, for a number whose visible label is elsewhere: the head of a table's column, a toolbar, the title of a row with a slider beside the input.
- **NMI-47** THE number input SHALL draw its box from the one set of tokens every input box is drawn from, the group comp.input-box (comp.input-box.container.shape, comp.input-box.container.height.medium, and so on): its container, its corners, its edges and their widths, the label that floats in it and the asterisk after it, the figure and the placeholder, the prefix and the suffix, the caret and the highlight of selected text, the paddings, the gaps and the least height of its size (a stacked box's from comp.input-box.stacked.container.height.medium and comp.input-box.stacked.container.padding-block.medium for size medium, as tall as a Button of the same size, and a filled or an outlined box's from comp.input-box.container.height.medium and comp.input-box.container.padding-block.medium, and so on), and the colors of each of these in its look, at rest, hovered, focused and disabled, plain and in error. It SHALL have no token of its own for any of them: the tokens of its own group say only where its steppers stand and how a held stepper repeats (NMI-25). WHEN a design system or a product sets a token of the box THE number input SHALL follow it, as the text input, the select, the combobox and the color input do, which read the same tokens; no token is held equal to another for this. WHERE one number input is to differ from the other boxes of a product, the token is set for that instance — on its element or on a subtree that holds it, in each platform's way of setting a token for a subtree — and THE number input SHALL draw itself from the value it finds there.
- **NMI-48** WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE number input SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the element that carries its role — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the number input is invalid — except those the number input owns (aria-disabled), which it SHALL move and not expose. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE number input SHALL take it off that element. WHEN the product sets an aria- attribute the number input owns THE number input SHALL report it in debug builds.

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.number-input.…` → `--tanrei-comp-number-input-…`).

#### `tanrei.comp.number-input`

| Token | Type | Value |
| --- | --- | --- |
| `comp.number-input.stepper.gap` | `dimension` | `{tanrei.sys.space.space0}` |
| `comp.number-input.stepper.inset` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.number-input.motion.repeat-delay` | `duration` | `{tanrei.sys.motion.duration.long2}` |
| `comp.number-input.motion.repeat-interval` | `duration` | `{tanrei.sys.motion.duration.short2}` |
| `comp.number-input.motion.repeat-interval-fast` | `duration` | `{tanrei.sys.motion.duration.short1}` |

#### `tanrei.comp.input-box`

| Token | Type | Value |
| --- | --- | --- |
| `comp.input-box.container.shape` | `dimension` | `{tanrei.sys.shape.corner.small}` |
| `comp.input-box.container.shape-bottom` | `dimension` | `{tanrei.sys.shape.corner.none}` |
| `comp.input-box.container.height.small` | `dimension` | `40px` |
| `comp.input-box.container.height.medium` | `dimension` | `48px` |
| `comp.input-box.container.height.large` | `dimension` | `56px` |
| `comp.input-box.container.padding-inline.small` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.input-box.container.padding-inline.medium` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.input-box.container.padding-inline.large` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.input-box.container.padding-inline-with-icon.small` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.input-box.container.padding-inline-with-icon.medium` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.input-box.container.padding-inline-with-icon.large` | `dimension` | `{tanrei.sys.space.space150}` |
| `comp.input-box.container.padding-block.small` | `dimension` | `8px` |
| `comp.input-box.container.padding-block.medium` | `dimension` | `10px` |
| `comp.input-box.container.padding-block.large` | `dimension` | `14px` |
| `comp.input-box.container.padding-block-with-label.small` | `dimension` | `2px` |
| `comp.input-box.container.padding-block-with-label.medium` | `dimension` | `4px` |
| `comp.input-box.container.padding-block-with-label.large` | `dimension` | `4px` |
| `comp.input-box.container.gap.small` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.input-box.container.gap.medium` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.input-box.container.gap.large` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.input-box.label.typescale.small` | `typography` | `{tanrei.sys.typescale.label}` |
| `comp.input-box.label.typescale.medium` | `typography` | `{tanrei.sys.typescale.label}` |
| `comp.input-box.label.typescale.large` | `typography` | `{tanrei.sys.typescale.label}` |
| `comp.input-box.label.floated-typescale.small` | `typography` | `{tanrei.sys.typescale.label-small}` |
| `comp.input-box.label.floated-typescale.medium` | `typography` | `{tanrei.sys.typescale.label-small}` |
| `comp.input-box.label.floated-typescale.large` | `typography` | `{tanrei.sys.typescale.label-small}` |
| `comp.input-box.requirement.asterisk-gap` | `dimension` | `{tanrei.sys.space.space25}` |
| `comp.input-box.input.typescale.small` | `typography` | `{tanrei.sys.typescale.body}` |
| `comp.input-box.input.typescale.medium` | `typography` | `{tanrei.sys.typescale.body-large}` |
| `comp.input-box.input.typescale.large` | `typography` | `{tanrei.sys.typescale.body-large}` |
| `comp.input-box.affix.gap` | `dimension` | `{tanrei.sys.space.space25}` |
| `comp.input-box.icon.size.small` | `dimension` | `{tanrei.sys.icon.small}` |
| `comp.input-box.icon.size.medium` | `dimension` | `{tanrei.sys.icon.medium}` |
| `comp.input-box.icon.size.large` | `dimension` | `{tanrei.sys.icon.medium}` |
| `comp.input-box.selection.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.input-box.selection.content.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.input-box.outline.width` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.input-box.outline.notch-padding` | `dimension` | `{tanrei.sys.space.space50}` |
| `comp.input-box.active-indicator.width` | `dimension` | `{tanrei.sys.border.hairline}` |
| `comp.input-box.stacked.container.color` | `color` | `{tanrei.sys.color.surface-container-highest}` |
| `comp.input-box.stacked.container.height.small` | `dimension` | `{tanrei.comp.button.container.height.small}` |
| `comp.input-box.stacked.container.height.medium` | `dimension` | `{tanrei.comp.button.container.height.medium}` |
| `comp.input-box.stacked.container.height.large` | `dimension` | `{tanrei.comp.button.container.height.large}` |
| `comp.input-box.stacked.container.padding-block.small` | `dimension` | `4px` |
| `comp.input-box.stacked.container.padding-block.medium` | `dimension` | `6px` |
| `comp.input-box.stacked.container.padding-block.large` | `dimension` | `14px` |
| `comp.input-box.stacked.active-indicator.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.input-box.stacked.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.input-box.stacked.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.input-box.stacked.input.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.input-box.stacked.placeholder.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.stacked.affix.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.stacked.leading-icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.stacked.trailing-icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.stacked.caret.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.input-box.stacked.required-asterisk.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.input-box.stacked.state-layer.color` | `color` | `{tanrei.comp.input-box.stacked.input.color}` |
| `comp.input-box.stacked.error.container.color` | `color` | `{tanrei.sys.color.surface-container-highest}` |
| `comp.input-box.stacked.error.active-indicator.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.input-box.stacked.error.outline.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.input-box.stacked.error.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.input-box.stacked.error.input.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.input-box.stacked.error.placeholder.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.stacked.error.affix.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.stacked.error.leading-icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.stacked.error.trailing-icon.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.input-box.stacked.error.caret.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.input-box.stacked.error.required-asterisk.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.input-box.stacked.error.state-layer.color` | `color` | `{tanrei.comp.input-box.stacked.error.input.color}` |
| `comp.input-box.filled.container.color` | `color` | `{tanrei.sys.color.surface-container-highest}` |
| `comp.input-box.filled.active-indicator.color` | `color` | `{tanrei.sys.color.outline}` |
| `comp.input-box.filled.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.input-box.filled.label.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.filled.input.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.input-box.filled.placeholder.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.filled.affix.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.filled.leading-icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.filled.trailing-icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.filled.caret.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.input-box.filled.required-asterisk.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.input-box.filled.state-layer.color` | `color` | `{tanrei.comp.input-box.filled.input.color}` |
| `comp.input-box.filled.error.container.color` | `color` | `{tanrei.sys.color.surface-container-highest}` |
| `comp.input-box.filled.error.active-indicator.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.input-box.filled.error.outline.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.input-box.filled.error.label.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.input-box.filled.error.input.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.input-box.filled.error.placeholder.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.filled.error.affix.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.filled.error.leading-icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.filled.error.trailing-icon.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.input-box.filled.error.caret.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.input-box.filled.error.required-asterisk.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.input-box.filled.error.state-layer.color` | `color` | `{tanrei.comp.input-box.filled.error.input.color}` |
| `comp.input-box.outlined.container.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.input-box.outlined.active-indicator.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.input-box.outlined.outline.color` | `color` | `{tanrei.sys.color.outline}` |
| `comp.input-box.outlined.label.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.outlined.input.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.input-box.outlined.placeholder.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.outlined.affix.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.outlined.leading-icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.outlined.trailing-icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.outlined.caret.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.input-box.outlined.required-asterisk.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.input-box.outlined.state-layer.color` | `color` | `{tanrei.comp.input-box.outlined.input.color}` |
| `comp.input-box.outlined.error.container.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.input-box.outlined.error.active-indicator.color` | `color` | `{"colorSpace":"srgb","components":[0,0,0],"alpha":0,"hex":"#000000"}` |
| `comp.input-box.outlined.error.outline.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.input-box.outlined.error.label.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.input-box.outlined.error.input.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.input-box.outlined.error.placeholder.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.outlined.error.affix.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.outlined.error.leading-icon.color` | `color` | `{tanrei.sys.color.on-surface-variant}` |
| `comp.input-box.outlined.error.trailing-icon.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.input-box.outlined.error.caret.color` | `color` | `{tanrei.sys.color.error}` |
| `comp.input-box.outlined.error.required-asterisk.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.input-box.outlined.error.state-layer.color` | `color` | `{tanrei.comp.input-box.outlined.error.input.color}` |
| `comp.input-box.motion.label-duration` | `duration` | `{tanrei.sys.motion.role.state.default}` |
| `comp.input-box.motion.label-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.state}` |
| `comp.input-box.motion.state-duration.small` | `duration` | `{tanrei.sys.motion.role.state.compact}` |
| `comp.input-box.motion.state-duration.medium` | `duration` | `{tanrei.sys.motion.role.state.default}` |
| `comp.input-box.motion.state-duration.large` | `duration` | `{tanrei.sys.motion.role.state.default}` |
| `comp.input-box.motion.state-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.state}` |

Per-state tokens, valued by the design system's pattern rules (not written by hand):

- `comp.input-box.<variant>.<state>.<property>` for the variants `stacked`, `filled`, `outlined`, the states `hovered`, `focused`, `disabled` and the properties `container.color`, `active-indicator.color`, `outline.color`, `label.color`, `input.color`, `placeholder.color`, `affix.color`, `leading-icon.color`, `trailing-icon.color`, `caret.color`, `required-asterisk.color`, `state-layer.color`, `error.container.color`, `error.active-indicator.color`, `error.outline.color`, `error.label.color`, `error.input.color`, `error.placeholder.color`, `error.affix.color`, `error.leading-icon.color`, `error.trailing-icon.color`, `error.caret.color`, `error.required-asterisk.color`, `error.state-layer.color`.
- `comp.input-box.<state>.<property>` for the states `hovered`, `focused`, `disabled` and the properties `opacity`, `outline.width`, `active-indicator.width`.

### What the web draws differently

- The element is form-associated. With a `name` it submits its value as a plain decimal number (`12.5`) whatever the locale shows, resets to its `value` attribute, is restored on navigation, follows a disabled `<fieldset>`, and reports a missing value while its field says it is required and a bad input while its text is not a number.
- The number is edited in a native `<input type="text">` in the shadow root, which carries the spin button’s role and values. It is not the browser’s number input.
- The element is a block. `focus()` goes to the native element, and a press anywhere on the box outside a stepper focuses it.
- `steppers` is the inverse attribute `no-steppers`, and `readOnly` is `readonly`.
- `input` is a CustomEvent with `detail.value`. `change` and `action` are plain events: read `value` from the element. Unless `action` is cancelled, the form is then asked to submit. `entryerror` is a CustomEvent whose `detail` holds `{ reason, text }`: `text` is the input’s `invalidText`, empty where none was given. The types are `TanreiNumberInputInputDetail` and `TanreiNumberInputEntryErrorDetail`.
- Parts: `container`, `outline`, `active-indicator`, `label`, `requirement-label`, `input`, `prefix`, `suffix`, `stepper`. It has no slot.
- Without `locale` the input reads the nearest `lang` attribute, then the browser’s language.
- Besides the locale’s separator, a period is read as the decimal separator wherever the locale does not group with periods.
- A page key steps ten steps on the step’s own grid, as a slider’s does: with a step of 2, Page Up from 42 gives 62, then 82.
- On iOS and iPadOS the input asks for the full keyboard while a negative number may be typed, because the decimal keypad there has no minus sign.
- `increment-label` and `decrement-label` left empty are read from the product’s words (`setWords()`).
- Before the element is defined it has no size. Reserve the height of the box.

## Guidelines

### Number input, number field, or a field by hand

| You need | Use |
| --- | --- |
| A label, the box, help and an error | A [number field](https://tanrei.chnotchy.com/components/number-field/) |
| The box alone, under a label that stands elsewhere: beside a slider, a table’s cell, a toolbar | A number input, named by its own `label` |
| Something of your own inside the frame, beside or below the box | A [field](https://tanrei.chnotchy.com/components/field/) with a number input and your own element in it |
| Digits that are not a quantity: a postal code, a card number | A [text field](https://tanrei.chnotchy.com/components/text-field/) |
| A value chosen by feel within a range | A [slider](https://tanrei.chnotchy.com/components/slider/) |

### Do

- Give a bare input a `label` in the words of the visible label, or in words that say what the visible label and its place say together: “Quantity, oak desk”.
- Give a bare input its width by its place: a column of a grid, a cell, a toolbar’s item with a width.
- Put the unit in `suffixText`, and give `valueText` where the suffix would be read badly.
- Give `incrementLabel` and `decrementLabel`, on the input or once for the whole product with `setWords()`.
- Use the number field wherever a label above the box, help and an error are what you need.

### Don’t

- Don’t use a bare input in a form to save the room of a label. An input with no visible label anywhere is a guess for everyone who sees it.
- Don’t use the placeholder as the label. It goes as soon as someone types, and it is never the name.
- Don’t set `label`, `error` or `disabled` on an input inside a field. Say them on the field, so that the label and the help follow.
- Don’t put a number field inside a field. It is a field already; the control a field frames is the number input.
- Don’t save on `input`. It fires at every keystroke; `change` carries the committed value.

## Accessibility

### What the component handles

- It is a spin button. Its name is its own `label`, or the label of the field around it where it has none. The placeholder is never the name.
- Its value is the committed number, its limits are `min` and `max`, and its value text is `valueText` or the number with its prefix and suffix.
- Its description is what its field gives: the help or the error text. Bare and in an entry error, it is `invalidText`.
- It is exposed as invalid while in error or while its text is not a number, as required while its field says so, and as read-only and disabled.
- The steppers are buttons named by `decrementLabel` and `incrementLabel`; one that nobody named is hidden from assistive technology. They are outside the focus order, because the arrow keys step from the input; a person exploring by touch still finds them.
- A press on a stepper does not move focus, so it does not raise the on-screen keyboard.
- While an input method is composing, the input reads nothing and handles no key. The wheel never changes the number.
- Focus is shown by the box’s own edge, from the keyboard and from a pointer alike. The hit area meets the minimum target, and each stepper is a full target.
- The asterisk after a floating label is hidden from assistive technology: the required state already says it.
- At 200% text a floating label wraps, and the number stays on one line and scrolls. In forced colors the box gains an edge. In a right-to-left layout the steppers and the label are mirrored: the steppers stand at the left end. The number and its unit are written as the unit’s own script writes them: a unit in Latin letters or signs after the number, from left to right, and a unit in the page’s script at the number’s left. What is read aloud does not change.
- Where the row cannot hold the number and the steppers side by side — at 200% text in a narrow window, with a prefix and a long suffix — the steppers move to a row of their own below the number, at the box’s end. They are never hidden, because on touch they are the only way to step.
- A missing name is reported in debug builds.

Known gaps on the web:

- Chromium reads the number without its unit: it takes the value text from the text in the input, and passes over the one the input names. Name the unit in the label where it matters.

### What you need to do

- Name every bare input with `label`. A visible text beside it does not name it.
- Where a bare input can be wrong, say what is wrong in words that reach assistive technology: a message of your own tied to the page, or a field around the input.
- Write `invalidText` even where no field shows it: it is read with the input.
- Give a slider and the number input beside it the same name.
