Forms

Digit Roll

A numeric field whose digits roll vertically on change, with horizontal scrubbing and keyboard stepping.

A number field with a typographic face. The value is rendered as a row of fixed width slots, each one tall enough for a single line, and only the characters that change roll out and in. Typing still works: a transparent input sits over the whole field, so the digits you see are decorative and the value you edit is the real control underneath.

The rolling direction follows the value. Increasing sends the old digit up and the new one in from below; decreasing does the opposite, and a digit that does not change stays still. That detail is what makes a multi-digit number read as one counter instead of several independent animations.

Usage

prefix and suffix are rendered inside the field and are stripped when a typed value is parsed. precision rounds both typed and stepped values, and step drives the arrow keys, the stepper buttons and the scrub distance.

<template>
  <div class="flex w-full justify-center py-6">
    <MDigitRoll
      class="w-52"
      :default-value="12"
      label="Tip"
      :max="100"
      :min="0"
      :precision="2"
      prefix="$"
      :step="0.5"
      stepper
    />
  </div>
</template>

Drag horizontally anywhere on the field to scrub the value. The gesture only starts after a few pixels of movement, so a click still places the caret.

Customize

Customize

Clamp on blur
Default Value0
Disabled
Format
LabelQuantity
Max100
Min0
Value42
Precision0
Prefix
Scrub
Step1
Stepper buttons
Suffix

Props

Prop Type Default Description
modelValuenumber-Controlled value. Leave it out to let the component keep its own.
defaultValuenumber0Starting value in uncontrolled usage.
minnumber-InfinitySmallest allowed value. Also the target of the Home key.
maxnumberInfinityLargest allowed value. Also the target of the End key.
stepnumber1Increment used by the arrow keys, the stepper buttons and scrubbing.
precisionnumber-Decimal places the value is rounded to, and the digits shown after the decimal separator.
formatIntl.NumberFormatOptions-Formatting options merged over the precision defaults, for currency, percent or unit suffixes.
prefixstring''Static text before the value.
suffixstring''Static text after the value.
labelstring-Visible label, also used as the accessible name.
stepperbooleanfalseRender the minus and plus buttons inside the field.
scrubbooleantrueAllow horizontal dragging to change the value.
clampOnBlurbooleantrueClamp a typed out-of-range value instead of rejecting it on commit.
disabledbooleanfalseDisable typing, stepping and scrubbing.
classstring-Extra classes for the outer wrapper.

Events

Event Payload Description
update:modelValue(value: number) => voidFired whenever the committed value changes, whether it came from typing, keys, the stepper or a scrub. Pair it with modelValue for controlled usage.

Accessibility

  • The real element is an <input> with role="spinbutton", aria-valuenow, aria-valuemin, aria-valuemax and a formatted aria-valuetext, so the spoken value matches what is on screen.
  • The visible digits are aria-hidden; they are a rendering of the value, not a second source of truth.
  • Up and Down step, Page Up and Page Down jump ten steps, Shift multiplies the step by ten, Alt divides it, and Home and End jump to the finite bounds.
  • The label is tied to the input with for/id; without a label the input falls back to the accessible name Number input.
  • An invalid commit sets aria-invalid and shakes the field, so the error is not conveyed by the red border alone. Under reduced motion the shake is skipped.
  • The stepper buttons are real buttons with aria-labels and become disabled at the bounds instead of silently doing nothing.

Performance

  • Only the characters that changed are mounted and animated. Unchanged digits keep their node, so a value like 1.10 to 1.11 animates one slot.
  • Intl.NumberFormat is built once per precision and format options, not on every render or keypress.
  • Scrubbing quantises the pointer to eight-pixel steps and only commits when the quantised value changes, so a drag does not fire a cascade of identical updates.
  • The roll itself is a short spring on y and opacity; there is no layout measurement and no per-frame JavaScript.
Copyright © 2026