Skip to content

NumberField

A number, typed or stepped by its buttons and the arrow keys.

Solidtype_heroui.Number_field is Kobalte’s NumberField.Root, with the classes HeroUI’s own numberFieldVariants gives, and the parts inside it as Kobalte’s. Its value is a number, controlled or its own, kept on its step’s grid within its range as HeroUI’s field keeps it: Kobalte snaps nothing, so the field snaps each value, a value halfway to the step away from the minimum, written with its step’s decimals, so a tenth added to 0.2 is 0.3; and it disables a button its step would not move. Its input is Kobalte’s spinbutton.

HeroUI’s examples, with HeroUI’s icons from @gravity-ui/icons.

number_field_usage.mlx
module Description = Solidtype_heroui.Description
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
let make () =
<Number_field
class_="w-full max-w-64" default_value=1024. min_value=0. name="width">
<Label>"Width"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
</Number_field>
<Number_field>
<Label>"Width"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"In pixels"</Description>
<Field_error>"Too wide"</Field_error>
</Number_field>
number_field_variants.mlx
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
let make () =
<div class_="flex flex-col gap-4">
<Number_field
default_value=100. min_value=0. name="primary-width" variant=`primary>
<Label>"Primary variant"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
</Number_field>
<Number_field
default_value=100. min_value=0. name="secondary-width" variant=`secondary>
<Label>"Secondary variant"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
</Number_field>
</div>
Enter the width in pixels
Value must be between 0 and 100
number_field_surface.mlx
module Description = Solidtype_heroui.Description
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
module Surface = Solidtype_heroui.Surface
module Format = Solidtype_kobalte.Types.NumberFormatOptions
let make () =
<Surface class_="flex w-full max-w-[280px] flex-col gap-4 rounded-3xl p-6">
<Number_field
default_value=1024. min_value=0. name="width" variant=`secondary>
<Label>"Width"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-full" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Enter the width in pixels"</Description>
</Number_field>
<Number_field
default_value=0.5
format_options=(fun () -> Format.make ~style:`percent ())
max_value=1.
min_value=0.
name="percentage"
step=0.1
variant=`secondary>
<Label>"Percentage"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-full" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Value must be between 0 and 100"</Description>
</Number_field>
</Surface>
Enter the width in pixels
Value must be between 0 and 100
number_field_description.mlx
module Description = Solidtype_heroui.Description
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
module Format = Solidtype_kobalte.Types.NumberFormatOptions
let make () =
<div class_="flex w-full max-w-64 flex-col gap-4">
<Number_field default_value=1024. min_value=0. name="width">
<Label>"Width"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Enter the width in pixels"</Description>
</Number_field>
<Number_field
default_value=0.5
format_options=(fun () -> Format.make ~style:`percent ())
max_value=1.
min_value=0.
name="percentage"
step=0.1>
<Label>"Percentage"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Value must be between 0 and 100"</Description>
</Number_field>
</div>
Rate from 1 to 10
number_field_required.mlx
module Description = Solidtype_heroui.Description
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
let make () =
<div class_="flex w-full max-w-64 flex-col gap-4">
<Number_field is_required=true min_value=0. name="quantity">
<Label>"Quantity"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
</Number_field>
<Number_field
is_required=true default_value=1. max_value=10. min_value=1. name="rating">
<Label>"Rating"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Rate from 1 to 10"</Description>
</Number_field>
</div>
Enter the width in pixels
Value must be between 0 and 100
number_field_disabled.mlx
module Description = Solidtype_heroui.Description
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
module Format = Solidtype_kobalte.Types.NumberFormatOptions
let make () =
<div class_="flex w-full max-w-64 flex-col gap-4">
<Number_field
is_disabled=true default_value=1024. min_value=0. name="width">
<Label>"Width"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Enter the width in pixels"</Description>
</Number_field>
<Number_field
is_disabled=true
default_value=0.5
format_options=(fun () -> Format.make ~style:`percent ())
max_value=1.
min_value=0.
name="percentage"
step=0.1>
<Label>"Percentage"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Value must be between 0 and 100"</Description>
</Number_field>
</div>
number_field_full_width.mlx
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
let make () =
<div class_="w-[400px] space-y-4">
<Number_field full_width=true default_value=1024. min_value=0. name="width">
<Label>"Width"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input />
<Number_field.Increment_button />
</Number_field.Group>
</Number_field>
</div>
Quantity must be greater than or equal to 0
Percentage must be between 0 and 100
number_field_validation.mlx
module Field_error = Solidtype_heroui.Field_error
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
module Format = Solidtype_kobalte.Types.NumberFormatOptions
let make () =
<div class_="flex w-full max-w-64 flex-col gap-4">
<Number_field
is_invalid=true is_required=true min_value=0. name="quantity" value=(-5.)>
<Label>"Quantity"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Field_error>"Quantity must be greater than or equal to 0"</Field_error>
</Number_field>
<Number_field
is_invalid=true
format_options=(fun () -> Format.make ~style:`percent ())
max_value=1.
min_value=0.
name="percentage"
step=0.1
value=1.5>
<Label>"Percentage"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Field_error>"Percentage must be between 0 and 100"</Field_error>
</Number_field>
</div>
Current value: 1024
number_field_controlled.mlx
module S = Solidtype.Solid
module Jsx = Solidtype.Jsx
module Button = Solidtype_heroui.Button
module Description = Solidtype_heroui.Description
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
let make () =
let value, set_value = S.create_signal 1024. in
<div class_="flex w-full max-w-64 flex-col gap-4">
<Number_field
min_value=0.
name="width"
value=(fun () -> S.get value)
on_change=(S.set set_value)>
<Label>"Width"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>
(fun () ->
Jsx.string ("Current value: " ^ Js.Float.toString (S.get value)))
</Description>
</Number_field>
<div class_="flex gap-2">
<Button variant=`tertiary on_click=(fun _ -> S.set set_value 0.)>
"Reset to 0"
</Button>
<Button variant=`tertiary on_click=(fun _ -> S.set set_value 2048.)>
"Set to 2048"
</Button>
</div>
</div>
Increments by 1
Increments by 5
Increments by 10
number_field_step.mlx
module Description = Solidtype_heroui.Description
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
let make () =
<div class_="flex w-full max-w-64 flex-col gap-4">
<Number_field
default_value=0. max_value=100. min_value=0. name="step1" step=1.>
<Label>"Step: 1"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Increments by 1"</Description>
</Number_field>
<Number_field
default_value=0. max_value=100. min_value=0. name="step5" step=5.>
<Label>"Step: 5"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Increments by 5"</Description>
</Number_field>
<Number_field
default_value=0. max_value=100. min_value=0. name="step10" step=10.>
<Label>"Step: 10"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Increments by 10"</Description>
</Number_field>
</div>

format_options is Kobalte’s own: the input shows and reads its value as a currency, a percent or a unit, in the reader’s locale, and tells a reader the value as it shows it.

Accounting format with EUR currency
Standard USD currency format
Percentage format (0-1, where 0.5 = 50%)
Decimal format with 2 decimal places
Unit format with kilograms
number_field_format.mlx
module Description = Solidtype_heroui.Description
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
module Format = Solidtype_kobalte.Types.NumberFormatOptions
let make () =
<div class_="flex w-full max-w-64 flex-col gap-4">
<Number_field
default_value=99.
min_value=0.
name="currency-eur"
format_options=(fun () ->
Format.make ~currency:"EUR" ~currency_sign:`accounting
~style:`currency ())>
<Label>"Currency (EUR - Accounting)"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Accounting format with EUR currency"</Description>
</Number_field>
<Number_field
default_value=99.99
min_value=0.
name="currency-usd"
format_options=(fun () -> Format.make ~currency:"USD" ~style:`currency ())>
<Label>"Currency (USD)"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Standard USD currency format"</Description>
</Number_field>
<Number_field
default_value=0.5
format_options=(fun () -> Format.make ~style:`percent ())
max_value=1.
min_value=0.
name="percentage"
step=0.01>
<Label>"Percentage"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Percentage format (0-1, where 0.5 = 50%)"</Description>
</Number_field>
<Number_field
default_value=1234.56
min_value=0.
name="decimal"
format_options=(fun () ->
Format.make ~maximum_fraction_digits:2.
~minimum_fraction_digits:2. ~style:`decimal ())>
<Label>"Decimal (2 decimal places)"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Decimal format with 2 decimal places"</Description>
</Number_field>
<Number_field
default_value=1000.
min_value=0.
name="unit"
format_options=(fun () ->
Format.make ~style:`unit ~unit:"kilogram"
~unit_display:`short ())>
<Label>"Unit (Kilograms)"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Description>"Unit format with kilograms"</Description>
</Number_field>
</div>

HeroUI’s button takes isPending while the order is sent; the port’s button has no pending state yet, so this one shows the spinner alone.

Only 3 items available
number_field_form.mlx
module S = Solidtype.Solid
module Jsx = Solidtype.Jsx
module Show = Solidtype.Show
module Button = Solidtype_heroui.Button
module Description = Solidtype_heroui.Description
module Field_error = Solidtype_heroui.Field_error
module Form = Solidtype_heroui.Form
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
module Spinner = Solidtype_heroui.Spinner
external prevent_default : Dom.event -> unit = "preventDefault" [@@mel.send]
let stock_available = 3.
let stock = Js.Float.toString stock_available
let make () =
(* Empty is not a number. *)
let value, set_value = S.create_signal Float.nan in
let is_submitting, set_is_submitting = S.create_signal false in
let is_out_of_stock () =
(not (Float.is_nan (S.get value))) && S.get value > stock_available
in
let unavailable () =
let v = S.get value in
Float.is_nan v || v < 1. || v > stock_available
in
let handle_submit e =
prevent_default e;
if not (unavailable ()) then (
S.set set_is_submitting true;
(* Simulate API call *)
ignore
(Js.Global.setTimeout
~f:(fun () ->
Js.log2 "Order submitted, quantity:" (S.get value);
S.set set_value Float.nan;
S.set set_is_submitting false)
1500
: Js.Global.timeoutId))
in
<Form class_="flex w-[280px] flex-col gap-4" on_submit=handle_submit>
<Number_field
is_required=true
is_invalid=(fun () -> is_out_of_stock ())
max_value=5.
min_value=1.
name="quantity"
value=(fun () -> S.get value)
on_change=(S.set set_value)>
<Label>"Order quantity"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Show
when_=(fun () -> is_out_of_stock ())
fallback=(<Description>
(fun () ->
Jsx.string ("Only " ^ stock ^ " items available"))
</Description>)>
<Field_error>
(fun () -> Jsx.string ("Only " ^ stock ^ " items left in stock"))
</Field_error>
</Show>
</Number_field>
<Button
class_="w-full"
is_disabled=(fun () -> unavailable ())
type_=`submit
variant=`primary>
<Show when_=(fun () -> not (S.get is_submitting))>"Place Order"</Show>
<Show when_=(fun () -> S.get is_submitting)>
<Spinner color=`current size=`sm /> "Processing..."
</Show>
</Button>
</Form>
Enter a value between 0 and 100
number_field_with_validation.mlx
module S = Solidtype.Solid
module Show = Solidtype.Show
module Description = Solidtype_heroui.Description
module Field_error = Solidtype_heroui.Field_error
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
module Format = Solidtype_kobalte.Types.NumberFormatOptions
let make () =
(* Empty is not a number. *)
let value, set_value = S.create_signal Float.nan in
let is_invalid () =
let v = S.get value in
(not (Float.is_nan v)) && (v < 0. || v > 100.)
in
<div class_="flex w-full max-w-64 flex-col gap-4">
<Number_field
is_required=true
format_options=(fun () -> Format.make ~style:`percent ())
is_invalid=(fun () -> is_invalid ())
max_value=1.
min_value=0.
name="percentage"
step=0.1
value=(fun () -> S.get value)
on_change=(S.set set_value)>
<Label>"Percentage"</Label>
<Number_field.Group>
<Number_field.Decrement_button />
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button />
</Number_field.Group>
<Show
when_=(fun () -> is_invalid ())
fallback=(<Description>"Enter a value between 0 and 100"</Description>)>
<Field_error>"Percentage must be between 0 and 100"</Field_error>
</Show>
</Number_field>
</div>
Custom icon children
number_field_custom_icons.mlx
module Description = Solidtype_heroui.Description
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
let make () =
<div class_="flex w-full max-w-64 flex-col gap-4">
<Number_field default_value=1024. min_value=0. name="width">
<Label>"Width (Custom Icons)"</Label>
<Number_field.Group>
<Number_field.Decrement_button>
<Gravity.Magnifier_minus />
</Number_field.Decrement_button>
<Number_field.Input class_="w-[120px]" />
<Number_field.Increment_button>
<Gravity.Magnifier_plus />
</Number_field.Increment_button>
</Number_field.Group>
<Description>"Custom icon children"</Description>
</Number_field>
</div>
number_field_chevrons.mlx
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
module Format = Solidtype_kobalte.Types.NumberFormatOptions
let make () =
<Number_field
class_="w-full max-w-64"
default_value=99.
min_value=0.
name="amount"
format_options=(fun () ->
Format.make ~currency:"EUR" ~currency_sign:`accounting
~style:`currency ())>
<Label>"Number field with chevrons"</Label>
<Number_field.Group class_="flex">
<Number_field.Input class_="flex-1" />
<div class_="flex h-full flex-col border-s border-field-placeholder/15">
<Number_field.Increment_button
class_="flex h-1/2 w-6 items-center justify-center rounded-none \
border-0 pt-0.5 text-sm">
<Gravity.Chevron_up width=11 height=11 />
</Number_field.Increment_button>
<Number_field.Decrement_button
class_="flex h-1/2 w-6 items-center justify-center rounded-none \
border-0 pb-0.5 text-sm">
<Gravity.Chevron_down width=11 height=11 />
</Number_field.Decrement_button>
</div>
</Number_field.Group>
</Number_field>

HeroUI’s Render Function example gives the field render, React’s way to swap the element it renders, which a Solid component has no need of.

class_ is added after HeroUI’s classes.

number_field_tailwind.mlx
module Label = Solidtype_heroui.Label
module Number_field = Solidtype_heroui.Number_field
let make () =
<Number_field
class_="w-full max-w-48"
default_value=2.
min_value=1.
name="guests"
variant=`secondary>
<Label class_="font-medium text-foreground">"Guests"</Label>
<Number_field.Group class_="rounded-xl bg-default">
<Number_field.Decrement_button class_="text-muted hover:text-foreground"
/>
<Number_field.Input class_="text-center tabular-nums" />
<Number_field.Increment_button class_="text-muted hover:text-foreground"
/>
</Number_field.Group>
</Number_field>
  • .number-field, and .number-field--primary, .number-field--secondary, .number-field--full-width
  • .number-field__group, and .number-field__group--full-width
  • .number-field__input
  • .number-field__increment-button, .number-field__decrement-button
  • Hover: [data-hovered="true"] on the group, the input and the buttons
  • Focus within: [data-focus-within="true"] on the group
  • Focus visible: [data-focus-visible="true"] on the input and buttons
  • Disabled: [data-disabled="true"]
  • Invalid: [data-invalid="true"]

Each prop beside the name HeroUI’s React component gives it.

React solidtype Type Default Description
variant variant [ `primary | `secondary ] `primary Its look
fullWidth full_width bool false Whether it takes its container’s width
value value float Its value, controlled
defaultValue default_value float Its value at first, kept by itself
onChange on_change float -> unit Told each change of its value
minValue min_value float The least value
maxValue max_value float The greatest value
step step float 1. How far a button or an arrow key steps
isDisabled is_disabled bool Whether it is disabled, else as its fieldset is
isInvalid is_invalid bool false Whether it is invalid, its error shown
isReadOnly is_read_only bool false Whether its value is kept
isRequired is_required bool false Whether it is required
name name string Its name in a form
aria-label aria_label string Its name where no label names it
className class_ string Added after HeroUI’s classes
onBlur on_blur Dom.focusEvent -> unit Called once focus leaves it for an element outside it
formatOptions format_options Solidtype_kobalte.Types.NumberFormatOptions.t How its value is shown and read
validate — A field validates nothing itself: its is_invalid and Field_error come from the application’s state

NumberField.Group, Input, IncrementButton, DecrementButton

Section titled “NumberField.Group, Input, IncrementButton, DecrementButton”

Each takes class_, added after HeroUI’s class; the input a placeholder; a button children, drawn in place of HeroUI’s plus or minus.

Original HeroUI documentation: NumberField.

The same HeroUI NumberField twice, under the same props: in solidtype on the left, in React on the right. Below them, every attribute of the two groups: a marked row is one they disagree on.

solidtype — OCaml, on Solid and Kobalte
React — TypeScript, on React Aria
attributesolidtypeReact