// preview in
saved for every page

Number field

A number input with minus and plus buttons. Keeps the value within bounds, steps with the keyboard and formats it as currency, percent or a unit.

Built on Base UIPattern WAI-ARIA Spinbutton (keyboard)Client componentSource ↗
minimal · light · #3fa34d
disabled
readOnly

Installation

Set up Camaleo once (Installation), then import what you need. Put it in a Field to give it a label.

import { NumberField, Field, FieldLabel } from "@camaleo/react";

Anatomy

One box with three parts: the minus button, the input and the plus button.

−2+
[data-slot="number-field"] · [data-slot="number-field-group"][data-slot="number-field-button"][data-slot="number-field-input"]

Examples

Every example is a real file you can copy. They all wear the skin, mode and brand from the bar above.

minimal · light · #3fa34d

Up to 4 tickets per order.

Minimum and maximum

Click plus until it stops. At max the button disables itself, and the description says why before people get there.

5 × $24.00 = $120.00 per month

Live total

Change the seats. value and onValueChange drive the price below, announced with aria-live.

Up to 50%. Shift + arrow jumps 25 points.

Percent, with steps

format={{ style: "percent" }} works on fractions, so step={0.05} is 5 points; largeStep is what Shift + arrow adds.

Currency

Pass format and locale and the input shows the currency symbol and separators people expect, while the value stays a number.

With a unit

A unit format puts "kg" inside the field, so the label doesn't have to say it and nobody guesses pounds.

Required

Type a number, delete it and leave. Inside a Field, required and FieldError match="valueMissing" work like on any input.

Named step buttons

The buttons only show − and +. decrementLabel and incrementLabel give them names, so translate them with the page.

In context

These are whole pieces of UI you can copy, in the skin you picked.

Your cart

$18.00

$16.50

Subtotal$52.50

Shopping cartCard · Field · NumberField · Button

Book your stay

Lakeside cabin · Oct 31 – Nov 3

Ages 2 to 12

BookingCard · Field · NumberField · Button
Package
Package sizeFieldset · Field · NumberField · NativeSelect

Guidelines

DoSmall amounts people adjust.

Guests, tickets, seats: a click or an arrow key gets there faster than typing.

Don’tCodes that are only digits.

ZIP codes, phone and card numbers aren't amounts: steppers and separators break them. Use an Input with inputMode="numeric".

DoShow the unit in the value.

"45 min" can't be misread.

Don’tA bare number.

Is 45 minutes, hours or people? Don't make people guess.

Props

PropTypeDefaultDescription
placeholderstring—Hint shown in the empty input. Not a replacement for a label.
decrementLabelstring"Decrease"Accessible name of the minus button. Translate it with the page.
incrementLabelstring"Increase"Accessible name of the plus button. Translate it with the page.
minnumber—The minimum value of the input element.
maxnumber—The maximum value of the input element.
stepnumber | "any"1Amount 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 min prop explicitly in conjunction with this prop. Specify step="any" to always disable step validation; interactive stepping then uses a base amount of 1, while the alt and shift keys still step by smallStep and largeStep.
largeStepnumber10The large step value of the input element when incrementing while the shift key is held. Snaps to multiples of this value when snapOnStep is enabled.
requiredbooleanfalseWhether the user must enter a value before submitting a form.
disabledbooleanfalseWhether the component should ignore user interaction.
readOnlybooleanfalseWhether the user should be unable to change the field value.
namestring—Identifies the field when a form is submitted.
valuenumber | null—The raw numeric value of the field.
defaultValuenumber—The uncontrolled value of the field when it's initially rendered. To render a controlled number field, use the value prop instead.
formatIntl.NumberFormatOptions—Options to format the input value.
onValueChange(value: number | null, details) => void—Runs on every value change, from typing, the buttons or the keyboard. details.reason says which.
localeIntl.LocalesArgument—The locale of the input element. Defaults to the user's runtime locale.

Base UI's other root props pass through, such as smallStep, snapOnStep, allowWheelScrub and onValueCommitted.

Styling hooks

Restyle it without forking: target the attributes, or set the tokens on a parent, a skin or the field itself.

Attributes

data-slot"number-field" · "number-field-group" · "number-field-input" · "number-field-button"
data-disabledon the root when disabled, on a button at its bound
data-readonlypresent when read-only
data-invalidpresent when invalid
data-scrubbingpresent while dragging to change the value

Tokens it reads

Color

--cml-border--cml-border-control--cml-danger--cml-fg--cml-field-bg--cml-field-border-sides--cml-field-shadow--cml-focus-offset--cml-focus-ring--cml-focus-width

Shape & size

--cml-field-border-width--cml-field-height--cml-field-padding--cml-font-size-field--cml-radius-control--cml-radius-field

Type & motion

--cml-duration-fast--cml-ease-standard--cml-font-field

This component only

--cml-corner-shape--cml-disabled-opacity--cml-number-btn-align--cml-number-btn-block--cml-number-btn-radius--cml-number-btn-shadow--cml-number-btn-size--cml-number-divider--cml-number-gap--cml-number-padding--cml-number-weight
/* round step buttons, only in the cart */
.cart [data-slot="number-field"] {
  --cml-number-btn-radius: 999px;
  --cml-number-divider: 0;
}

Accessibility

Arrow Up · Arrow DownAdds or subtracts one step.
Shift + ArrowAdds or subtracts largeStep.
Home · EndJumps to min or max, when they are set.
TabMoves focus to the input. The step buttons are skipped, since the arrows do the same.
Was this page helpful? · Edit this page on GitHub