NumberInput

A field for a number that hands back a number — never a string.

import { ... } from '@corndev/ui'

onChange received: 8080 (number)

HTML

<label class="corn-label" for="port">Port</label>
<div class="corn-input-wrapper">
  <input id="port" class="corn-input" data-size="md" type="text" inputmode="numeric" role="spinbutton" aria-valuemin="1" aria-valuemax="65535" value="8080" />
  <span class="corn-input-section" data-position="right">
    <span class="corn-number-input-controls">
      <button type="button" class="corn-number-input-step" tabindex="-1" aria-label="Decrease">−</button>
      <button type="button" class="corn-number-input-step" tabindex="-1" aria-label="Increase">+</button>
    </span>
  </span>
</div>

Classes

Class
.corn-input
.corn-number-input-controls
.corn-number-input-step
.corn-input-wrapper

Vanilla JS

Required for behaviour — adds the parsing that keeps the value a number, stepping with the arrow keys, Page Up/Down, Home/End and the buttons, and clamping on blur.

import { createNumberInput } from '@corndev/ui';
import '@corndev/ui/theme';

const el = document.querySelector('#target');
const instance = createNumberInput(el, { /* options */ });

Options

OptionTypeDefaultDescription
valuenumberundefinedThe value. Pass it to update() to drive the field from outside; undefined empties it.
defaultValuenumberundefinedInitial value.
onChange(value: number|undefined) => voidundefinedCalled with a number, or undefined when the field is emptied. Never a string, and not called for text still on its way to a number, like -.
minnumberundefined
maxnumberundefined
stepnumber1How far the arrow keys and the buttons move it.
allowDecimalbooleantrue
allowNegativebooleanundefinedDefaults to true unless min is 0 or more.
hideControlsbooleanfalseLeave out the − and + buttons; the arrow keys still step.
size'sm'|'md'|'lg''md'
disabledbooleanfalse
errorbooleanfalse
fullWidthbooleanfalse

React

NumberInput — from @corndev/ui-react.

Props

PropTypeDefaultDescription
valuenumberundefinedThe value (controlled). undefined is a value — an empty field — so passing the prop at all makes it controlled.
defaultValuenumberundefinedInitial value (uncontrolled).
onChange(value: number|undefined) => voidundefinedCalled with a number, or undefined when the field is emptied. Never a string, and not called for text still on its way to a number, like -.
minnumberundefined
maxnumberundefined
stepnumber1How far the arrow keys and the buttons move it.
allowDecimalbooleantrue
allowNegativebooleanundefinedDefaults to true unless min is 0 or more.
hideControlsbooleanfalseLeave out the − and + buttons; the arrow keys still step.
size'sm'|'md'|'lg''md'
disabledbooleanfalse
errorbooleanfalse
fullWidthbooleanfalse
leftSectionReact.ReactNodeundefinedA unit or an icon before the number.
classNamestringundefinedOn the wrapper, which draws the field.
styleobjectundefinedOn the wrapper, so a width lands on the field.

onChange receives a number, or undefined when the field is emptied — never a string. That is what this component is for. A text field bound to a numeric value hands back "1" for 1, and a form that writes that back has silently turned data into text the moment someone clicked the field.

<NumberInput aria-label="Port" value={port} onChange={setPort} min={1} max={65535} />

Typing, and clamping

Text on its way to a number — -, 1. — may stand while you type it, and is not reported until it is a number. Anything that can never be a number is refused.

The value is clamped on blur, not on every keystroke, so you can type 50 on the way to 5 in a field whose max is 10 without it snapping under you.

It is a text input with the ARIA spinbutton role rather than type="number", whose value is "" for exactly the partial entries above.

Stepping

Key
↑ ↓Step by step
Page Up Page DownTen steps
Home Endmin, max

The − and + buttons step too, and are left out of the tab order because the keys already do the job — two extra stops per number field make a form slow to cross. hideControls removes them. Decimal steps don’t drift: 0.2 + 0.1 is 0.3.

Options

  • min, max, step (default 1)
  • allowDecimal (default true) and allowNegative (default true, unless min is 0 or more)
  • Everything Input takes — size, error, disabled, fullWidth, leftSection for a unit — and like Input with a section, className and style go on the wrapper that draws the field.

In React, passing value at all makes it controlled. undefined is a real value — an empty field — so a form that clears a controlled field back to undefined clears it.