Skip to content
EMBOSS
Docs menu

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/stepper

Usage

example.tsx
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.

PropTypeDefaultDescription
value / defaultValue / onValueChangenumber / number / (value) => voidControlled and uncontrolled usage.
min / max / stepnumber / number / numberRange and step of the field.
disabledbooleanfalseDisables the field and keycaps.

Keyboard

KeysAction
ArrowUpArrowDownSteps the value.
HomeEndJumps to min or max.

Source

View source — stepper.tsx
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 };