InputNumber

A field for a number, like a price, a count or a percentage, with a range, a precision and stepper buttons.

Accessibility

What InputNumber does for you, and what it leaves to the page around it.

What it renders

The field is a real <input> with role="spinbutton", inside a div that holds the affixes and the buttons. The kit changes none of it except the button icons.
  • aria-valuemin and aria-valuemax are your min and max. Leave those out and a screen reader announces a spin button with no range, so declare the range wherever one exists.
  • aria-valuenow is the current value, and it is absent while the field is empty or holds text that is not a number.
  • There is no aria-valuetext. A formatter changes only what is drawn, so a field showing $ 1,000 still announces 1000. The unit is never read out, so if it matters, put it in the field's label as well as in a suffix.
  • Attributes you pass land on the <input>, so id, aria-label, aria-describedby, aria-invalid, inputMode and autoComplete all reach the element with the role. Mouse handlers are the exception and go to the outer element.

Labeling it

The component renders no label of its own. A spin button with no name is announced as “spinbutton, 12”, and the reader is left asking 12 of what.
  • Inside a Form.Item the label is wired for you, includinghtmlFor. Prefer it, since it also connects the validation message.
  • Outside one, use a real <label htmlFor> pointing at the field's id. It names the field and makes the label text a click target, which an aria-label does not.
  • aria-label is for a field with no visible text beside it, like a quantity cell in a table row. Name the quantity and its subject, “Quantity, Blue T-shirt”, rather than repeating “Quantity” down a column.
  • A prefix or suffix is not a label. It draws a symbol inside the field and adds nothing to the field's name.
  • Hang help text, the unit and the range off aria-describedby. “Between 1 and 99” read once on focus beats a number that changes on blur without a word.

Keyboard

  • Tab reaches the field, and the field is the only tab stop the component has.
  • Up and Down step by step, and the page does not scroll underneath.
  • Shift with Up or Down steps by ten times step. It stands in for the large step, so mention it in your help text.
  • Page Up and Page Down do nothing, although the ARIA spin button pattern suggests them for a large step. Leave them out of your help text and your tests.
  • Home and End move the caret, as in any text field, rather than jumping to min or max.
  • Enter commits what has been typed, clamps it into the range and fires onPressEnter. It keeps working with keyboard={false}, which only turns off arrow stepping.

The stepper buttons

Each button is a <span role="button"> with an aria-label and no tabIndex, so it has a name but cannot be reached with Tab.
  • A keyboard user cannot move to one and press it. The arrow keys are the keyboard equivalent, so with keyboard={false} typing is the only route left.
  • They act on mousedown rather than on click, so Enter and Space would do nothing even with focus. Holding one repeats the step.
  • Their names are the fixed English strings Increase Value and Decrease Value. They do not follow ConfigProvider's locale, so in a localized product they stay in English.
  • The kit replaces only the icon inside each span, through controls. The role, the name and the disabled state stay as they are. An icon you pass as upIcon sits inside a named button, so it needs no name of its own.
  • At a bound, the button that would leave the range gains aria-disabled and stays in place, so it does not move under a pointer that is holding it down.

Announcing a clamped value

Type 500 into a field whose max is 100 and, on blur, the value becomes 100. onChange fires, aria-valuenow updates, and nothing is announced. A screen reader user who has already moved on never learns the number changed.
  • Put the range in the field's description, wired with aria-describedby, so it is read before the value is entered rather than after it is corrected.
  • Where the correction matters, announce it with a polite live region, or with Form.Item's validation message, which is already tied to the field.
  • While the reader types, an out-of-range value only changes the text color and sets no ARIA. To have it announced, set aria-invalid yourself when the value is outside min and max.
  • status only paints a border color. Never let status be the only signal. Pair it with a message, and inside a Form.Item let the form own both.

Contrast and motion

  • The button icons are the smallest marks the component draws, and in mode="input" two of them share the height of one field. Check them against the field's background rather than the page, since they are the first to fail when you change the theme's colors.
  • A disabled button at a bound is a grayed icon. That works as a hint but not as the whole message, which is what aria-disabled and the field's description are for.
  • A prefix or suffix usually renders in a secondary text color. Where it carries meaning, like a unit, it needs the same 4.5:1 contrast as the value beside it.
  • variant="borderless" removes the only mark that says the field is editable. Keep some other cue, like a background change or a hover edge, or a reader cannot find the field at all.
  • The only motion is the field's own short border and shadow transition, and it follows motion: false in your token file's seed. Put anything you animate on top of it behind motion-safe:.