Skip to content

Number

A native number input with visible steppers, rendered as <input type="number" is="root-number">.

Usage

<input type="number" is="root-number" data-label="Quantity" min="0" max="10" step="1" value="1" />

The component wraps the input and injects decrement/increment buttons wired to the native stepUp/stepDown, honouring min/max/step/disabled/ readonly and disabling a stepper at its bound. The input stays a native spinbutton - value, keyboard (arrow keys) and form participation are untouched; the native browser spin buttons are hidden in favour of the component's.

Both buttons sit in the row's own tab order, after the field: tabbing in reaches the value, then -, then +. - is still drawn to the left of the value - it is put back there with order, which is the one place this composition lets the visual order and the DOM order disagree, against a positive tabindex on all three. Arrow keys still step the value with the input alone focused - the buttons are there for a keyboard user who reaches for them directly, not a replacement for it.

The box is Input's box: the same 2px Neutral 400 frame on the page's own ground, the same xl radius, the same 40px height and the same inline padding, from the same User Input/Fields/* variables. It used to be its own thing - a 1px Neutral 200 frame around a Neutral 100 fill at the 2xs radius, 44px tall, with the two steppers on a band of their own - so a quantity field and a text field in one form read as two different systems. The steppers sit on the field's ground inside that frame now, which is what makes the box read as one control with two buttons in it rather than three boxes in a row.

Sizes

data-size takes small and large; the default is the medium step and has no value to write. Input's steps, value for value - 36 / 40 / 44px, with the corner stepping up at large - so the two line up wherever they share a row.

<input type="number" is="root-number" data-size="large" data-label="Large" value="1" />
<input type="number" is="root-number" data-label="Medium" value="1" />
<input type="number" is="root-number" data-size="small" data-label="Small" value="1" />

Bounds

A stepper disables itself once the value reaches its bound, so the pair says what the range is without a message. The value stays reachable by typing and by the arrow keys, which is the native spinbutton doing its own job.

<input type="number" is="root-number" data-label="At the minimum" min="0" max="3" value="0" />
<input type="number" is="root-number" data-label="At the maximum" min="0" max="3" value="3" />

Hint and error

data-hint holds the description and data-error the error message: both are authored once, and data-invalid decides which one shows. With no data-error the browser's own validationMessage stands in. One node carries both, wired to the control through aria-describedby.

<input type="number" is="root-number" data-label="Guests" data-hint="Up to four per booking" min="1" max="4" value="2" />
<input type="number" is="root-number" data-label="Guests" data-error="Four at most" data-invalid="true" min="1" max="4" value="9" />

Required

required is the native attribute; the asterisk beside the label is drawn from it and hidden from assistive tech, which reads the control's own required instead.

<input type="number" is="root-number" required data-label="Guests" min="1" max="4" />

States

Hover moves the border one step deeper and focus takes the Default step, exactly as on Input - the border moving is the indicator, so focus draws no ring. disabled repaints the box, takes the not-allowed cursor and stills the steppers; readonly carries no styling of its own, the value being selectable and the field still focusable.

<input type="number" is="root-number" data-label="Tab to me" value="2" min="1" max="4" />
<input type="number" is="root-number" data-label="Disabled" value="3" disabled />
<input type="number" is="root-number" data-label="Read-only" value="3" readonly />

Inline layout

inline on the <label> lays the label text and the field on one row instead of stacking them. It has to sit on the label, which means authoring the label yourself - the generated one takes no attributes from the control.

<label inline>
  <span data-part="label">Guests</span>
  <span field-wrapper>
    <input type="number" is="root-number" data-size="small" value="2" min="1" max="4" />
  </span>
</label>

Attributes

Attribute Type Default Description
min / max number - Bounds; a stepper disables itself at its own
step number 1 Step increment
data-size 'small' | 'large' medium Field height, radius and label scale
data-label string - Label text; the component builds the <label>
required boolean false Marks the field required and draws the asterisk
data-hint string - Description shown under the field
data-error string - Error message; falls back to validationMessage
data-invalid 'true' - Shows the error instead of the description
disabled boolean false Disables the field and both steppers
readonly boolean false Read-only state; the steppers do nothing

Methods

Method Description
increment() / decrement() Step the value and re-evaluate the stepper bounds
updateButtons() Disable a stepper at its bound

Events

Native input / change fire from the underlying <input>.

Extends

HTMLInputElement - use with <input type="number" is="root-number">.