Skip to content
NewService Map: 30 components for hosts, ports and routes
<UiOdometer />ReactDataFree

Odometer stepper

Number stepper where every digit is its own rolling wheel; supports press-and-hold, sideways drag, the scroll wheel and the keyboard.

View as Markdown
<UiOdometer />

Tryhold +, drag sideways on the number, or scroll over it.

Playground

Change a prop and the component re-renders. Props marked remounts set an initial value, so the component starts over.

Loading <UiOdometer />
Props
steplive
labellive
unitlive
React
<UiOdometer
  defaultValue={8192}
  min={1024}
  max={65536}
  step={1024}
  label="Max output tokens"
/>

Installation

pnpm dlx shadcn@latest add @lumesec/ui-odometer
First timeRegister the @lumesec registry once, or install by URL+

Adds @lumesec to your components.json:

pnpm dlx shadcn@latest registry add @lumesec=https://elements.lumesec.ai/r/{name}.json

Or skip the setup and install by URL:

npx shadcn@latest add https://elements.lumesec.ai/r/ui-odometer.json

Usage

ui-odometer-example.tsx
import { UiOdometer } from "@/components/lumesec/ui-odometer";

export function Example() {
  return <UiOdometer defaultValue={8192} min={1024} max={65536} step={1024} label="Max output tokens" />;
}

Behaviour

Every digit is its own wheel, and changes ripple from the right like a mechanical counter. Columns and the thousands separator slide in when the number gets longer.

API reference

Props

Also accepts every prop of <div> (React.ComponentProps<"div">), spread onto the root element.

valueTypenumber

Controlled value, rounded to the nearest multiple of step and clamped to min/max. Use with onValueChange.

defaultValueTypenumberDefault8192

Initial value when uncontrolled.

minTypenumberDefault1024

Lowest value.

maxTypenumberDefault65536

Highest value. Also sets the number of digit wheels.

stepTypenumberDefault1024

Increment per step.

labelTypestringDefault"Max output tokens"

Caption above the number and the spinbutton's aria-label.

unitTypestring

Optional monospace caption under the number; hidden when empty.

onValueChangeType(value: number) => void

Called on every value change from the buttons, drag, wheel or keys.

onValueCommitType(value: number) => void

Called with each change from the buttons, wheel or keys, and once when a drag on the number ends, even if the value did not change.

Ref

ref points at the root HTMLDivElement.

Keyboard

KeysAction
ArrowUp / ArrowRightIncrease by one step.
ArrowDown / ArrowLeftDecrease by one step.
PageUp / PageDownChange by ten steps, stopping at max or min.
Home / EndJump to min / max.

Accessibility

  • The number has role="spinbutton", tabindex="0", aria-valuenow, aria-valuemin, aria-valuemax and an aria-label from label.
  • The minus and plus buttons are native buttons labelled "Decrease" and "Increase". They get aria-disabled="true" at the limits but stay clickable.
  • unit is not part of the spinbutton's accessible value.
  • The digit wheels are aria-hidden; the spinbutton's aria-valuetext carries the number as shown, for example "8,192".
  • Reduced motion: the digit wheels, column widths and separators change without transitions, and pushing past a limit does not shake.

Theming

Styled with Tailwind classes on your shadcn theme tokens, so light and dark follow your theme. The accent comes from --lumesec. See Theming.

This component reads --foreground, --muted-foreground, --card, --muted, --border and --lumesec.

Notes

  • Holding a button repeats after 380 ms and speeds up; after 14 repeats it moves four steps at a time.
  • Dragging sideways on the number changes it by one step per 12 px. The scroll wheel over the number changes one step per tick and does not scroll the page; sideways scrolling is ignored.
  • Digits are grouped with a comma every three places. The display is built for non-negative whole numbers.
  • Changing value from outside rolls the wheels without calling either callback.

Was this page helpful?