Number field
A real spinbutton in place of the native number input - steppers you can style, hold-to-repeat, wheel while focused, and a value the browser cannot sanitise out from under you.
The default spinner
A text input carrying role="spinbutton", not <input type="number">, so the steppers look the same in every browser and a half-typed value survives. Arrows step, shift+arrow steps by big_step, Home and End jump to the bounds. The input is the only tab stop; the buttons sit at tabindex="-1" with labels, the way the ARIA spinbutton pattern asks.
<.number_field name="quantity" value="12" min={0} max={99} />
Split buttons
Minus at the start, plus at the end, value centred - the cart-quantity anatomy. At a bound the button greys out with aria-disabled rather than disabled, so it keeps its name for a screen reader instead of vanishing. This one sits at its minimum.
<.number_field name="cart_quantity" value="1" min={1} max={10} variant="split" />
Addons and precision
Leading and trailing addons ride the same input-group surface the rest of the library uses. precision rounds and pads on blur while the raw text stands while you type - the whole built-in formatting story. For currency or percent display, format on blur with Intl.NumberFormat; the moduledoc has the pattern.
<div class="flex flex-col gap-4">
<.number_field name="price" value="24.5" min={0} step={0.5} precision={2}>
<:leading>$</:leading>
</.number_field>
<.number_field name="allocation" value="25" min={0} max={100} step={5} big_step={25}>
<:trailing>%</:trailing>
</.number_field>
</div>
Sizes
sm, md and lg move the input density and the button hit areas together, so the control stays square with the inputs beside it.
<div class="flex flex-col gap-4">
<.number_field name="size_sm" value="1" size="sm" />
<.number_field name="size_md" value="1" size="md" />
<.number_field name="size_lg" value="1" size="lg" />
</div>
Plain, and disabled
variant="plain" drops the buttons for keyboard, wheel and typing only - the dense-table flavour. disabled uses the native attribute on the input and both buttons, so nothing is reachable by pointer or keyboard.
<div class="flex flex-col gap-4">
<.number_field name="plain_qty" value="7" variant="plain" min={0} max={100} />
<.number_field name="disabled_qty" value="7" disabled />
</div>
In a form field
type="number-field" wires it into <.field>, so the label, help text and error tone come from the shared form-field machinery - the error ring is painted by the wrapper on the input-group surface, with no number-field-specific styling.
must be at least 1
<div class="flex flex-col gap-2">
<.field
type="number-field"
name="seats"
value="3"
label="Seats"
min={1}
max={20}
help_text="Between 1 and 20."
/>
<.field
type="number-field"
name="seats_error"
value="0"
label="Seats"
min={1}
errors={["must be at least 1"]}
/>
</div>
Properties
| Attribute | Type | Default | Description |
|---|---|---|---|
big_step
|
any |
nil
|
increment for shift+arrow and page up/down; defaults to step * 10 |
class
|
any |
nil
|
extra classes for the field surface |
decrement_label
|
string |
"Decrease value"
|
accessible name for the decrement button |
disabled
|
boolean |
false
|
disables the input and both buttons natively |
field
|
{:struct, Phoenix.HTML.FormField} |
a form field struct, e.g. @form[:quantity]; sets id, name and value like other inputs | |
id
|
any |
nil
|
input id; generated from the field or name if not passed |
increment_label
|
string |
"Increase value"
|
accessible name for the increment button |
max
|
any |
nil
|
upper bound; clamped and mirrored to aria-valuemax |
min
|
any |
nil
|
lower bound; values are clamped and mirrored to aria-valuemin |
name
|
any |
input name; generated from the field if not passed | |
precision
|
integer |
nil
|
decimal places shown on blur; the raw text stands while editing. nil means no formatting |
rest
|
global |
all other attributes land on the input | |
size
|
string |
"md"
|
input height and text size
one of: "sm", "md", "lg"
|
step
|
any |
1
|
increment for the buttons, arrow keys and wheel |
value
|
any |
current value; generated from the field if not passed | |
variant
|
string |
"stacked"
|
stacked: both buttons at the inline end; split: minus at the start, plus at the end; plain: no buttons
one of: "stacked", "split", "plain"
|
:leading
slot
|
slot |
addon before the input, e.g. a currency symbol | |
:trailing
slot
|
slot |
addon after the input, e.g. a unit |