Input Number

A number field with buttons that step the value down and up. It has Input's height, border and states, comes in five layouts, keeps to a minimum and a maximum, and shows Western digits in every language.

Installation

bash
npx @smicolon/fasla-ui@latest add input-number

Run init once per project before adding components. See Installation.

Preview

Variants

Five layouts, chosen with variant. default puts two Icon Buttons after the value. vertically-stacked stacks plus over minus in a narrow column, horizontally-stacked puts minus and plus side by side after the value, and horizontally-stretched puts minus before the value and plus after it. These three draw a 1px divider on the side of each button that faces the value. mini is 128px wide, for table cells and toolbars. Every layout is 36px tall, with the value centred.

States

The states are Input's. Focus is a real state, not a prop: tab to the field, or press a button. status="success" draws the border and the label in the success token, and invalid in the destructive token; the focus halo takes the same token. disabled turns off the field and both buttons, and dims the whole field to half opacity.

Limits and steps

min and max keep the value in range. At a limit, the matching button is disabled and stays in place, so the layout never shifts. step sets how far each press and each arrow key moves the value; Shift with an arrow moves it by 10.

Digits and formatting

The value is always drawn in Western digits, 0 to 9, in English and in Arabic, and a value typed in Arabic-Indic digits is read as the same number. format takes the options of Intl.NumberFormat, for a currency or a percentage, and locale sets the separators. The value keeps its own direction, so a minus sign stays before the digits on a right-to-left page.

Label

label puts a label above the field and links it to the input. Where the context already names the field, as in a table row, leave the label out and pass aria-label instead.

Accessibility

  • The value is a text input with a numeric keyboard on phones. A hidden input submits the number with its form, under name.
  • ArrowUp and ArrowDown step the value, Shift with an arrow moves it by 10, and Home and End jump to min and max when they are set.
  • The buttons are named Increase and Decrease, or زيادة and إنقاص under an Arabic lang. Rename them with incrementLabel and decrementLabel. They stay out of the tab order, since the arrow keys do the same from the input, and pressing and holding one keeps stepping.
  • Don’t use vertically-stacked on touch screens: its buttons are 23px wide and 17px tall, smaller than the 24px minimum touch target. Use horizontally-stacked or horizontally-stretched instead.
  • label names the input. Without a visible label, pass aria-label or aria-labelledby.
  • Direction is inherited from dir, never a prop. The layout uses logical properties only, so in Arabic the buttons and their dividers move to the other side and minus and plus trade places. The stacked layout keeps plus on top.

Props

InputNumber

PropTypeDefaultDescription
variant"default" | "vertically-stacked" | "horizontally-stacked" | "horizontally-stretched" | "mini""default"Where the buttons sit: five layouts.
valuenumber | null—The value, when you control it. Use it with onValueChange.
defaultValuenumber—The value on first render.
onValueChange(value, details) => void—Called with the new number, or null when the field is cleared.
minnumber—The lowest value. The decrement button is disabled there.
maxnumber—The highest value. The increment button is disabled there.
stepnumber | "any"1How far each press and each arrow key moves the value.
formatIntl.NumberFormatOptions—Options for Intl.NumberFormat, such as a currency or a percentage. Digits stay Western.
localeIntl.LocalesArgument—The locale for separators and symbols. Default: the reader's.
placeholderstring—Text shown while the field is empty.
labelReactNode—The field's label, linked to the input for you.
aria-labelstring—The input's accessible name when there is no visible label.
aria-labelledbystring—The id of an element that names the input, in place of label.
incrementLabelstring—The increment button's name. Default: Increase, or زيادة in Arabic.
decrementLabelstring—The decrement button's name. Default: Decrease, or إنقاص in Arabic.
status"default" | "success""default"Pass success to mark a value that passed validation.
invalidbooleanfalseMarks the field invalid; aria-invalid does the same.
disabledbooleanfalseDisables the field and both buttons, and dims the whole group.
readOnlybooleanfalseShows the value but stops it changing.
requiredbooleanfalseThe form submits only once the field has a value.
namestring—The name the value is submitted under.
formstring—The id of the form the value is submitted with, when the field sits outside it.
idstring—The input's id, for a label element of your own.
classNamestring—Extra CSS classes for the field group.

Usage

Basic number field

tsx
import { InputNumber } from "@/components/ui/input-number"

export function QuantityField() {
  return <InputNumber name="quantity" label="Quantity" defaultValue={1} min={1} max={10} />
}

Controlled, with limits

tsx
"use client"

import * as React from "react"
import { InputNumber } from "@/components/ui/input-number"

export function GuestsField() {
  const [guests, setGuests] = React.useState<number | null>(2)
  return (
    <InputNumber
      variant="horizontally-stretched"
      label="Guests"
      value={guests}
      onValueChange={setGuests}
      min={1}
      max={8}
    />
  )
}

Currency format

tsx
import { InputNumber } from "@/components/ui/input-number"

export function PriceField() {
  return (
    <InputNumber
      name="price"
      label="Price"
      format={{ style: "currency", currency: "SAR" }}
      defaultValue={1250}
      min={0}
      step={50}
    />
  )
}