import { Field, FieldLabel } from "@/components/ui/field";
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function NumberFieldBasic() {
return (
<Field className="w-28">
<FieldLabel>Seats</FieldLabel>
<NumberField defaultValue={3} min={1} max={10}>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
</Field>
);
}- Add the registryOnce per project
pnpm dlx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json - Add number-field
pnpm dlx shadcn@latest add @jarvis/number-field
Usage
import { Field, FieldLabel } from "@/components/ui/field";
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export function GuestsField() {
return (
<Field>
<FieldLabel>Guests</FieldLabel>
<NumberField defaultValue={2} min={1} max={12}>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
</Field>
);
}A FieldLabel in the same Field names the input. Without a visible label, give
NumberFieldInput an aria-label.
Composition
Examples
Formatting
format takes Intl.NumberFormat
options, for decimals, percentages or units.
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function NumberFieldFormatting() {
return (
<div className="flex flex-wrap items-center gap-3">
<NumberField
defaultValue={50}
min={5}
step={5}
format={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }}
className="w-36"
>
<NumberFieldGroup>
<span>$</span>
<NumberFieldInput aria-label="Amount in USD" />
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
<NumberField
defaultValue={0.15}
min={0}
max={1}
step={0.05}
format={{ style: "percent" }}
className="w-28"
>
<NumberFieldGroup>
<NumberFieldInput aria-label="Discount" />
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
</div>
);
}Scrub area
Wrap the label in a NumberFieldScrubArea to change the value by dragging it sideways.
import { Field, FieldLabel } from "@/components/ui/field";
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
NumberFieldScrubArea,
} from "@/components/ui/number-field";
export default function NumberFieldScrub() {
return (
<div className="flex flex-wrap items-center gap-3">
<Field className="w-32">
<NumberField defaultValue={240} min={0} max={1200}>
<NumberFieldScrubArea>
<FieldLabel>Width</FieldLabel>
</NumberFieldScrubArea>
<NumberFieldGroup className="pr-2">
<NumberFieldInput />
<span>px</span>
</NumberFieldGroup>
</NumberField>
</Field>
<Field className="w-32">
<NumberField defaultValue={0.8} min={0} max={1} step={0.01} format={{ style: "percent" }}>
<NumberFieldScrubArea>
<FieldLabel>Opacity</FieldLabel>
</NumberFieldScrubArea>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
</Field>
</div>
);
}Large
Set size="lg" on the group for a taller field.
import * as React from "react";
import { Button } from "@/components/ui/button";
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { toast } from "@/components/ui/toast";
const cards = { visa: "Visa •• 7391", amex: "Amex •• 1008" };
export default function NumberFieldLarge() {
const [amount, setAmount] = React.useState<number | null>(50);
return (
<div className="flex flex-wrap items-center gap-3">
<NumberField
value={amount}
onValueChange={setAmount}
min={5}
max={500}
step={5}
format={{ minimumFractionDigits: 2, maximumFractionDigits: 2 }}
className="w-40"
>
<NumberFieldGroup size="lg">
<span>$</span>
<NumberFieldInput aria-label="Amount in USD" />
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
<Select items={cards} defaultValue="visa">
<SelectTrigger size="lg" aria-label="Payment method">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(cards).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="secondary"
size="lg"
disabled={amount === null}
onClick={() => toast.add({ title: "Funds added", type: "success" })}
>
Add ${amount === null ? "" : amount.toFixed(2)}
</Button>
</div>
);
}Disabled
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function NumberFieldDisabled() {
return (
<NumberField defaultValue={12} disabled className="w-28">
<NumberFieldGroup>
<NumberFieldInput aria-label="Quantity" />
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
);
}Read-only
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function NumberFieldReadOnly() {
return (
<NumberField defaultValue={12} readOnly className="w-28">
<NumberFieldGroup>
<NumberFieldInput aria-label="Quantity" />
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
);
}Invalid
Set invalid on the Field and explain the problem with a FieldError.
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import {
NumberField,
NumberFieldDecrement,
NumberFieldGroup,
NumberFieldIncrement,
NumberFieldInput,
} from "@/components/ui/number-field";
export default function NumberFieldInvalid() {
return (
<Field invalid className="w-40">
<FieldLabel>Quantity</FieldLabel>
<NumberField defaultValue={0} min={0}>
<NumberFieldGroup>
<NumberFieldInput />
<NumberFieldDecrement />
<NumberFieldIncrement />
</NumberFieldGroup>
</NumberField>
<FieldError>Order at least one.</FieldError>
</Field>
);
}Props
The tables leave out className, style and render. Parts built on Base UI take them as its styling and composition guides describe.
NumberField
| Prop | Type | Default |
|---|---|---|
idThe id of the input element. | string | — |
minThe minimum value of the input element. | number | — |
maxThe maximum value of the input element. | number | — |
allowOutOfRangeWhen true, direct text entry may be outside the | boolean | false |
smallStepThe small step value of the input element when incrementing while the alt key is held.
Snaps to multiples of this value when | number | 0.1 |
stepAmount to increment and decrement with the buttons and arrow keys, or to scrub with pointer movement in the scrub area.
To always enable step validation on form submission, specify the | number | "any" | 1 |
largeStepThe large step value of the input element when incrementing while the shift key is held.
Snaps to multiples of this value when | number | 10 |
requiredWhether the user must enter a value before submitting a form. | boolean | false |
disabledWhether the component should ignore user interaction. | boolean | false |
readOnlyWhether the user should be unable to change the field value. | boolean | false |
nameIdentifies the field when a form is submitted. | string | — |
formIdentifies the form that owns the hidden input. Useful when the number field is rendered outside the form. | string | — |
valueThe raw numeric value of the field. | number | null | — |
defaultValueThe uncontrolled value of the field when it's initially rendered. To render a controlled number field, use the | number | — |
allowWheelScrubWhether to allow the user to scrub the input value with the mouse wheel while focused and hovering over the input. | boolean | false |
snapOnStepWhether the value should snap to the nearest step when incrementing or decrementing. | boolean | false |
formatOptions to format the input value. | Intl.NumberFormatOptions | — |
onValueChangeCallback fired when the number value changes. The
| ((value: number | null, eventDetails: NumberField.Root.ChangeEventDetails) => void) | — |
onValueCommittedCallback function that is fired when the value is committed.
It runs later than
It runs simultaneously with Warning: This is a generic event not a change event. | ((value: number | null, eventDetails: NumberField.Root.CommitEventDetails) => void) | — |
localeThe locale of the input element. Defaults to the user's runtime locale. | Intl.LocalesArgument | — |
inputRefA ref to access the hidden input element. | React.Ref<HTMLInputElement> | — |
NumberFieldGroup
| Prop | Type | Default |
|---|---|---|
size | "default" | "lg" | "default" |
NumberFieldInput
| Prop | Type | Default |
|---|---|---|
aria-roledescriptionA user-friendly description of the input's role for assistive tech. This is a role
description, not an accessible name — use | string | 'Number field' |
NumberFieldDecrement
| Prop | Type | Default |
|---|---|---|
nativeButtonWhether the component renders a native | boolean | true |
NumberFieldIncrement
| Prop | Type | Default |
|---|---|---|
nativeButtonWhether the component renders a native | boolean | true |
NumberFieldScrubArea
| Prop | Type | Default |
|---|---|---|
directionCursor movement direction in the scrub area. | "horizontal" | "vertical" | "horizontal" |
pixelSensitivityDetermines how many pixels the cursor must move before the value changes. A higher value will make scrubbing less sensitive. | number | 2 |
teleportDistanceIf specified, determines the distance that the cursor may move from the center of the scrub area before it will loop back around. | number | — |