Skip to content
Tier

Number input

Pro

A field for one number, with buttons to step it down and up. Arrow Up and Arrow Down step it from the keyboard, a screen reader hears a spin button with its value, and the phone shows a number pad. Empty is null, never 0: nothing is filled in for the person.

The number is read and shown in LocaleProvider's locale, not the browser's: "1,5" is one and a half in German, and digits in any script (Arabic-Indic, Persian, Devanagari, Bengali …) are read too. What the person types stays as typed until they leave the field, unless the value changes from outside to a different number, or to null from a number. Text that isn't a number is already null, so only its form's reset clears it (an uncontrolled field then goes back to defaultValue). With name, a form gets the number in plain digits ("1.5"), whatever the locale.

The step buttons are for a pointer and stay out of the tab order (the arrow keys do the same from the field). At min or max the button that would pass it is locked and says why (in a tooltip: needs a TooltipProvider), and an arrow key pressed at the limit says the same to a screen reader. A field that is disabled, readOnly or aria-disabled (as Locked makes it) doesn't change by any route: the step buttons, the arrow keys and the scrub grip do nothing, the buttons are aria-disabled and described by what describes the field (Locked's reason), and an aria-disabled field takes no typing either. Values typed outside the range are kept as typed, for your validation to name; the buttons and arrow keys stop at the range. From empty, the first step goes to 0, or to the nearest end of the range when 0 is outside it.

Range checks are your app's. The field is a text field, so it carries no min, max or step for the browser: form.checkValidity(), :invalid and :out-of-range don't catch an out-of-range number. Check the number you get from onValueChange and give Field the error.

Use Field for the label, hint and error, and unit for what the number counts ("minutes", "USD"); the unit is read with the field. A phone's number pad has no minus sign, so with a min below 0 (or none) the field asks for the full keyboard.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1 and 2.6.2: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/number-input

Brings in: button, input, locale, locked, motion, tokens, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/candor/ui/card"
import { Field } from "@/components/candor/ui/field"
import { NumberInput } from "@/components/candor/ui/number-input"
import { TooltipProvider } from "@/components/candor/ui/tooltip"

// Empty is null, not 0. The buttons stop at the range and say why; a typed value outside it is left for your check to name.
function TimeLimit() {
  const [minutes, setMinutes] = React.useState<number | null>(15)
  const error = minutes !== null && (minutes < 1 || minutes > 120) ? "Enter a time limit from 1 to 120 minutes" : undefined
  return (
    <Field label="Time limit per run" hint="The agent stops and reports what it finished." error={error} className="w-full max-w-xs">
      <NumberInput value={minutes} onValueChange={setMinutes} min={1} max={120} step={5} unit="minutes" />
    </Field>
  )
}

// A fine-tune card: an inspector built from Card and NumberInputs with `scrub`. Drag a grip sideways to change its
// number (Shift for ten steps at a time); the arrow keys and the step buttons do the same from the keyboard.
function FineTune() {
  const [temperature, setTemperature] = React.useState<number | null>(0.7)
  const [tokens, setTokens] = React.useState<number | null>(1200)
  const [retries, setRetries] = React.useState<number | null>(2)
  return (
    <Card className="w-full max-w-xs">
      <CardHeader>
        <CardTitle>Fine-tune</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-3">
        <Field label="Temperature">
          <NumberInput value={temperature} onValueChange={setTemperature} min={0} max={2} step={0.1} scrub />
        </Field>
        <Field label="Reply length">
          <NumberInput value={tokens} onValueChange={setTokens} min={100} max={8000} step={100} unit="tokens" scrub />
        </Field>
        <Field label="Retries">
          <NumberInput value={retries} onValueChange={setRetries} min={0} max={5} scrub />
        </Field>
      </CardContent>
    </Card>
  )
}

export default function Example() {
  return (
    <TooltipProvider>
      <div className="flex w-full flex-col gap-6">
        <TimeLimit />
        <FineTune />
      </div>
    </TooltipProvider>
  )
}

<NumberInput>

PropTypeDefaultDescription
valuenumber | null | undefined—The number, when you control it. `null` is empty.
defaultValuenumber | undefined—The number at first, when you don't control it. Left out, the field is empty.
onValueChange((value: number | null) => void) | undefined—Called with the number as the person types or steps, or `null` when the field is emptied or what is typed isn't a number.
minnumber | undefined—The lowest the buttons and arrow keys go. 0 or more also brings up the phone's number pad. A typed number below it is your validation's to catch: the browser isn't told.
maxnumber | undefined—The highest the buttons and arrow keys go. A typed number above it is your validation's to catch: the browser isn't told.
stepnumber | undefined1How much a button press or arrow key changes the number. Defaults to 1.
unitstring | undefined—What the number counts, printed after it and read with the field: "minutes", "USD".
attendboolean | undefined—Rings the field once, as `Input`'s `attend` does, after a failed submit.
scrubboolean | undefinedfalseAdds a grip at the field's start: dragging it sideways changes the number by a step for every 4 pixels (ten steps with Shift), towards the end of the line to raise it (the left in a right-to-left page), and stops at `min` and `max`. For an inspector's fine tuning; the arrow keys and the step buttons stay the way to it for the keyboard and screen readers, and the grip is hidden from them.
namestring | undefined—The form field's name. The form gets the number in plain digits ("1.5"), or "" when empty.
"aria-describedby"string | undefined—Ids of anything else that describes the field; the unit's id is added after them.

Also takes the props of Omit<React.ComponentProps<"input">, "value" | "defaultValue" | "type" | "onChange" | "min" | "max" | "step" | "inputMode">.

Strings

The fixed words, in English by default. Change any of them under numberInput in <LocaleProvider strings={{ … }}>; the table is exported as numberInputStrings.

KeyEnglish
decreaseDecrease
increaseIncrease
atMinAlready at the lowest, {min}.
atMaxAlready at the highest, {max}.

Motion

MomentFamilyWhat it does
scrubclockWith `scrub`, while the person drags the grip sideways the number follows the pointer, a step for every 4 pixels, painted from the pointer itself, with no motion between steps.
valuevalue, fadeWhen the person presses a step button and the number changes, the field's number crossfades to the new one, carrying which way it went for the Pro number drawings. Typing, the arrow keys and a field drawn with its value stay still, and `attend` clearing doesn't replay the last step's.

Set with --cd-motion-number-input-<moment>-* in your brand file; the families are in the design reference.