Built on Vercel’s UI SDK 0.1.0-beta.2. Its API may change.
Number input
A number field on Vercel's UI SDK with steppers that repeat while held, currency, percent and unit formats in any locale, and a drag-to-scrub handle.
- Form components
- 8 parts
- 13 examples
@tabler/icons-react@ui-sdk/core@ui-sdk/reactclass-variance-authority
pnpm dlx shadcn@latest add https://hextaui.com/r/ui-sdk-number-input.jsonAdds the component, the HextaUI theme tokens and any HextaUI components it depends on.
Add the theme tokens to your global CSS, if you haven’t yet.
Install the dependencies.
pnpm add @ui-sdk/react@0.1.0-beta.2 @ui-sdk/core@0.1.0-beta.2 @tabler/icons-react class-variance-authority cnCopy and paste the following code into your project.
components/ui/ui-sdk-number-input.tsx Update the import paths to match your project setup.
UI SDK adds a little CSS of its own in a cascade layer named ui-sdk. Declare that layer at the top of your global stylesheet, above the Tailwind import, so your classes win over it.
The value is a number, or null when the field is empty, while the field shows it formatted. Typing updates the value once the text is a valid number. Enter or leaving the field commits it: the text is read, kept within min and max, snapped to the step and formatted again.
A step button turns off at its bound. A typed value outside the bounds is pulled back in when it’s committed.
step is the usual increment. Shift, Page Up and Page Down move by largeStep, and Alt or Option with an arrow key by smallStep. Hold a button to repeat.
format takes Intl.NumberFormat options. People can type in the same format, and a percent is a fraction, so 15% is 0.15. The variant="stacked" group puts small chevrons at the end.
locale picks separators, symbols and digit grouping, for showing and for typing. Without it, the field follows the page’s language once it’s interactive, and the server renders English.
Drag a NumberInputScrubArea sideways to change the value, like in a design tool. Where the browser allows pointer lock, the drag can go past the edge of the screen, and NumberInputScrubAreaCursor draws the cursor.
With wheel, scrolling over the focused field steps the value. It’s off by default, so a page that scrolls past the field never changes it.
onValueChange runs on every valid change, and onValueCommit once an interaction ends, which is the one to save on. A controller steps or sets the value from any button.
With a name, the number submits as is, like 1250, never formatted. required blocks an empty field, and a form reset returns to the default.
invalid marks the field and sets aria-invalid on the input, for checks of your own like stock.
A disabled field can’t be focused or changed, and doesn’t submit.
A read-only field can be focused and submits, but steps, scrubs and typing don’t change it.
The buttons swap sides, and the locale sets the digits and the currency.
| Key | Action |
|---|---|
| ↑↓ | Steps the value by step. |
| ShiftPageUpPageDown | Steps by largeStep, ten steps by default. Shift works with the arrows, the buttons, the wheel and a scrub. |
| Alt | With an arrow key, steps by smallStep, a tenth of a step by default. |
| HomeEnd | Goes to the minimum or the maximum, when set. |
| Enter | Commits and formats what’s typed. |
The field follows the keyboard of the WAI-ARIA spinbutton pattern on a native text input, so ordinary text editing keeps working. Name the input with a label or aria-label. The step buttons are named “Increase” and “Decrease” by default, point at the input, and stay out of the tab order because the arrow keys do the same.
A field with decimals asks for a decimal keypad on phones, and the step buttons grow on touch screens. They only shrink on press when motion is allowed.
Each part wraps the UI SDK part of the same name and accepts all of its props, including render and a className that can be a function of the part’s state.
| Prop | Type | Default |
|---|---|---|
value | number | null | – |
defaultValue | number | null | – |
onValueChange | (value: number | null, details) => void | – |
onValueCommitRuns once an interaction ends. For saving. | (value: number | null, details) => void | – |
min | number | – |
max | number | – |
step | number | 1 |
largeStep | number | step × 10 |
smallStep | number | step ÷ 10 |
format | Intl.NumberFormatOptions | – |
localeDefaults to the page’s language. | string | – |
wheel | boolean | false |
invalid | boolean | false |
disabled | boolean | false |
readOnly | boolean | false |
required | boolean | false |
name | string | – |
form | string | – |
controllerFrom createNumberInputController(). | NumberInputController | – |
| Attribute | Description |
|---|---|
data-slot="number-input" | Target the number input in CSS. |
data-scrubbing | Present while a scrub changes the value. |
data-invalid | Present when invalid. |
data-disabled | Present when disabled. |
data-readonly | Present when read-only. |
data-required | Present when required. |
| Prop | Type | Default |
|---|---|---|
variantButtons on both sides, chevrons stacked at the end, or no buttons, for a scrub handle. | "split" | "stacked" | "plain" | "split" |
render | ReactElement | (props, state) => ReactElement | – |
| Attribute | Description |
|---|---|
data-slot="number-input-group" | Target the field’s box in CSS. |
data-variant | The variant: split, stacked or plain. |
| Prop | Type | Default |
|---|---|---|
id | string | – |
placeholder | string | – |
render | ReactElement | (props, state) => ReactElement | – |
| Prop | Type | Default |
|---|---|---|
aria-label | string | "Increase" |
disabledTurns off this button alone. | boolean | – |
childrenDefaults to a plus, or a chevron in the stacked variant. | ReactNode | (state) => ReactNode | – |
render | ReactElement | (props, state) => ReactElement | – |
| Attribute | Description |
|---|---|
data-slot="number-input-increment" | Target the button in CSS. |
data-at-bound | Present when the value is at max. |
data-disabled | Present when the button can’t step. |
| Prop | Type | Default |
|---|---|---|
aria-label | string | "Decrease" |
disabledTurns off this button alone. | boolean | – |
render | ReactElement | (props, state) => ReactElement | – |
| Attribute | Description |
|---|---|
data-slot="number-input-decrement" | Target the button in CSS. |
data-at-bound | Present when the value is at min. |
Stacks the increment above the decrement at the end of a stacked group.
| Prop | Type | Default |
|---|---|---|
orientationRight and up increase the value. | "horizontal" | "vertical" | "horizontal" |
pixelsPerStepHow far the pointer moves for one step. | number | – |
controllerConnects a scrub area outside the number input, like an icon in a toolbar. | NumberInputController | – |
| Attribute | Description |
|---|---|
data-slot="number-input-scrub-area" | Target the scrub area in CSS. |
data-orientation | horizontal or vertical. |
data-scrubbing | Present while a drag changes the value. |
| Prop | Type | Default |
|---|---|---|
childrenDefaults to a sideways arrow, centered on the pointer. | ReactNode | – |
- CalendarA date grid for single, range and multiple selection, with sliding months, range previews and touch-sized days.
- CheckboxA checkbox whose check draws in, with indeterminate parents, groups and labels that share its hover.
- ComboboxA filterable select with chips, groups and async results, in a popup that resizes as you type.
- Date pickerA button that opens a calendar in a popover, or a bottom sheet on phones, for single dates and ranges.
- FieldLabels, descriptions and errors wired to their control, with validation states and layouts for forms.
- InputA text input with three sizes, invalid and read-only states, native validation styling and a 16px touch font so phones never zoom in.