Number Flow
An odometer for dashboards: three fixed cells, one digit each, and the digits
that change slide vertically in the direction the value moved. Increasing sends
the old digit up, decreasing sends it down, and untouched places stay put, so
128 to 129 animates a single cell.
The readout is deliberately fixed-width. Values below one hundred show a leading zero, which keeps the cells from reflowing as the number grows and gives a counter the steady face of an instrument rather than the ragged edge of a sentence. The value itself is exposed to assistive technology separately, so the leading zeros are never read out.
Usage
The buttons are the only input. They disable themselves at min and max, and
the component emits update:modelValue so it can be controlled from anywhere.
Orders shipped
<template>
<div class="flex w-full flex-col items-center gap-3 py-6">
<p class="text-sm text-muted">
Orders shipped
</p>
<MNumberFlow
v-model="count"
:max="999"
:min="0"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(128)
</script>
Rapid clicks are safe: the component keeps its own cursor of the committed value, so two clicks in the same tick count twice even when a parent has not re-rendered yet.
Customize
Customize
buttonClass styles both stepper buttons and digitClass restyles the row of
cells without touching the readout logic.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | number | - | Controlled value. Leave it out to let the component keep its own, starting at zero. |
min | number | 0 | Smallest allowed value. |
max | number | 999 | Largest allowed value. |
digitClass | string | - | Extra classes for the row of digit cells. |
buttonClass | string | - | Extra classes for each stepper button. |
class | string | - | Extra classes for the outer wrapper. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | (value: number) => void | Fired on every step. Pair it with modelValue for controlled usage. |
Accessibility
- The numeric value is rendered in a visually hidden
<output>with an accessible name, while the digit cells arearia-hidden. A screen reader gets one exact number instead of three decorative glyphs. - Both buttons are real
<button>elements witharia-labels, so their purpose does not depend on the plus and minus icons. - The buttons become disabled at the bounds, which is announced and also stops
the value from drifting outside
min/max. - Nothing is conveyed by colour alone; the value is always in text.
- Under
prefers-reduced-motionthe digits swap instantly rather than rolling.
Performance
- Each place is keyed by its own digit, so only the cells whose digit changed remount and animate. Unchanged places keep their DOM node.
- The counter has no timer, no interval and no animation loop. Work happens on a button press and then stops.
- There is no text measurement, no resize observer and no layout read; the three cells are fixed width by design.
- The transition is a 300ms tween on
transformandopacity, both of which stay off the main thread.