Skip to content

NumberEditor

Edits any numeric field as a number field, with the parse, the format and the step chosen by the data type.

Terminal window
pnpm dlx shadcn@latest add https://sg-widgets.vercel.app/r/react/number-editor.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://sg-widgets.vercel.app/r/svelte/number-editor.json
NumberEditor: one control over six data types

React

Loading the React demo…

Svelte

Loading the Svelte demo…

data type typed emitted
number, percent 42 42
number, percent 3.7 nothing; whole numbers only
float 1.23456789 "1.234568"
float 2 "2.0"
currency 12500.5 12500.5
duration 90 90
duration 1h 30m, 1.5h, 1:30 90
duration 2d, with an eight-hour day 960
timecode 3600000 3600000
timecode 01:00:00 3600000
timecode 00:00:01:00, at 23.976 1000
any empty null

A duration shows the minutes it will store under the input while you type.

The value is written in the locale’s own marks, so a currency reads 12,500.50 and a number reads 1,001. What is emitted keeps the API’s shape. A typed unit form is parsed before it is formatted, so 1h 30m becomes 1:30 on commit.

The two buttons flanking the input, the arrow keys and the scrub area all move the value by one step. The step is the unit the data type reads in:

data type step bounds
number, currency 1 the 32-bit bounds
float 0.1 none
percent 1 0 to 100
duration 15 minutes the 32-bit bounds
timecode one frame at frameRate, one second without one the 32-bit bounds

A step runs on the parsed value, so 1h 30m in the input steps to 1:45. A stepper goes disabled at the bound it would cross. An empty field lands on zero on its first step.

proptypedefaultmeaning
valuenumber | string | nullnullThe stored value. Two-way in Svelte.
onValueChange(value: number | string | null) => void—Called when the input commits.
dataType'number' | 'float' | 'percent' | 'duration' | 'timecode' | 'currency''number'Picks the parse, the format, the affix and the step.
fieldPick<FieldSchema, 'displayName' | 'mandatory'> | nullnullSupplies the accessible label and the required flag.
precisionnumber6Decimals kept on a float or a currency.
hoursPerDaynumber—The site's working day. Without it the day unit on a duration is refused.
frameRatenumber—Frames per second, for the frame form of a timecode and its one-frame step.
symbolstring'$'Shown before the value on a currency field.
min / maxnumber0 and 100 on a percent, the 32-bit bounds elsewhereRange the value is held in.
stepnumberthe data type'sWhat one step moves.
scrubbooleanfalseName the control and let a drag across that name change the value.
labelstringthe field's display nameThe name over a scrub area.
localestringthe runtime'sLocale the value is written in.
hintbooleanfalseShow the stored form under the control, such as the minutes behind a duration.
disabledbooleanfalse
readonlybooleanfalse
invalidbooleanfalseForced invalid state.
errorstring | nullnullA message from the caller, shown in place of the parse error.
onErrorChange(error: string | null) => void—Called when the parse error appears or clears.
placeholderstring—
class / classNamestring—Merged after the widget's own classes.
errorMessagefrom ValueEditor(message: string) => ReactNode / Snippet<[string]>—Renders the message. Default is a small destructive line under the control.
sizefrom ValueEditor'sm' | 'md' | 'lg''md'Control height.
inlinebooleanfalseCompact for one row: a fixed width for the data type, the steppers inside the input, no hint.

onValueChange fires on commit only. Input that does not parse emits nothing; it sets the invalid state and reports the message through onErrorChange.

errorMessage receives the message and renders it.

keydoes
Up / DownSteps once.
Shift + Up / DownSteps ten times.
Page Up / Page DownSteps a hundred times.
EnterCommits.
EscapeRestores the stored value.
TabCommits by leaving the control.
Enterfrom ValueEditorCommits the draft. In a textarea it adds a line, and commitOnEnter turns the commit off.
Escapefrom ValueEditorRestores the stored value and drops what the last parse said.
Tabfrom ValueEditorLeaves the control, which commits.

The steppers are out of the tab order: the input reaches every value they do. Holding one down repeats after 400ms, then every 60ms.

A number, a percent and a timecode take whole numbers and reject a decimal outright, while a duration truncates one toward zero at 200; all four sit in a signed 32-bit column (field_types/number, percent, timecode, duration).

A float is returned as a quoted string rounded to six decimals, and rejects an integer on write and inside a filter, so a whole value is emitted with a decimal point (field_types/float).

A duration is a whole number of minutes and the field names no unit. The site does: GET /preferences carries hours_per_day and duration_units (field_types/duration).

A timecode is milliseconds. No schema or preference names the frame rate; a _summarize grouping is the one place the server renders one, and the rate solves out of that (field_types/timecode).

The corpus has no card for currency. It is parsed as a decimal here, and its write shape is not verified.

The React half composes Base UI’s NumberField 1.8.0 for the group, the steppers and the scrub area. Bits UI has no number field, so the Svelte half is written here against the same parts, classes and attributes; its state model — the step and its Shift and Page multipliers, the bounds, the hold that repeats, the pixel-per-step scrub — follows Zag’s number-input 1.43.3, which is not installed.