Components Number field

Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

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.

heex
<.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.

heex
<.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.

$
%
heex
<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.

heex
<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.

heex
<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.

Between 1 and 20.

must be at least 1

heex
<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