Stepper
A machined number input — a mono readout milled into the chassis, flanked by keycap buttons that auto-repeat when held.
@emboss/stepper
Installation
pnpm dlx shadcn@latest add @emboss/stepperUsage
import { Stepper } from "@/components/ui/stepper";
export function Example() {
return <Stepper defaultValue={64} min={0} max={127} aria-label="Velocity" />;
}API reference
Stepper
Extends the underlying number field root. The readout is also a scrub area — drag it horizontally to change the value.
| Prop | Type | Default | Description |
|---|---|---|---|
| value / defaultValue / onValueChange | number / number / (value) => void | — | Controlled and uncontrolled usage. |
| min / max / step | number / number / number | — | Range and step of the field. |
| disabled | boolean | false | Disables the field and keycaps. |
Keyboard
| Keys | Action |
|---|---|
| ArrowUpArrowDown | Steps the value. |
| HomeEnd | Jumps to min or max. |
Source
View source — stepper.tsxHide source — stepper.tsx
"use client";
import { NumberField as BaseNumberField } from "@base-ui/react/number-field";
import { Minus, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
export type StepperProps = React.ComponentProps<typeof BaseNumberField.Root>;
/**
* A machined number input: a mono readout milled into the chassis, flanked
* by keycap buttons that auto-repeat with acceleration when held. The
* readout doubles as a scrub area — drag it to change the value.
*
* @example
* <Stepper defaultValue={64} min={0} max={127} aria-label="MIDI velocity" />
*/
function Stepper({
className,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
...props
}: StepperProps) {
return (
<BaseNumberField.Root
data-slot="stepper"
className={cn("inline-flex", className)}
{...props}
>
<BaseNumberField.Group className="inline-flex items-stretch gap-1 rounded-md bg-surface-2 p-1 shadow-emboss-1">
<BaseNumberField.Decrement
data-slot="stepper-decrement"
aria-label="Decrease"
className="inline-flex w-7 cursor-pointer items-center justify-center rounded-sm bg-surface-2 text-ink-muted shadow-emboss-1 focus-ring transition-[box-shadow,translate] duration-(--duration-press) ease-(--ease-press) select-none hover:text-ink active:translate-[calc(var(--away-x)*1px)_calc(var(--away-y)*1px)] active:shadow-flush data-disabled:pointer-events-none data-disabled:opacity-50"
>
<Minus className="size-3.5" aria-hidden />
</BaseNumberField.Decrement>
<BaseNumberField.ScrubArea className="cursor-ew-resize">
<BaseNumberField.Input
data-slot="stepper-input"
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
className="h-7 w-16 rounded-sm bg-well text-center font-mono text-sm text-ink tabular-nums shadow-deboss-1 focus-ring selection:bg-accent selection:text-accent-fg data-disabled:opacity-50"
/>
</BaseNumberField.ScrubArea>
<BaseNumberField.Increment
data-slot="stepper-increment"
aria-label="Increase"
className="inline-flex w-7 cursor-pointer items-center justify-center rounded-sm bg-surface-2 text-ink-muted shadow-emboss-1 focus-ring transition-[box-shadow,translate] duration-(--duration-press) ease-(--ease-press) select-none hover:text-ink active:translate-[calc(var(--away-x)*1px)_calc(var(--away-y)*1px)] active:shadow-flush data-disabled:pointer-events-none data-disabled:opacity-50"
>
<Plus className="size-3.5" aria-hidden />
</BaseNumberField.Increment>
</BaseNumberField.Group>
</BaseNumberField.Root>
);
}
export { Stepper };