Skip to content
All Taste components

Inputs

Number stepper

A small count you nudge up or down, with limits that disable the buttons.

default · at minimum · with sub-label · small

Adults
Ages 13 or above
Children
Ages 2–12
Infants
Under 2
Masala dosa · ₹120
"use client";
import { Minus, Plus } from "lucide-react";
import { cn } from "@/lib/utils";

const sizes = {
  sm: { box: "h-[30px]", btn: "w-7 [&_svg]:size-3.5", input: "w-8 text-[13px]" },
  md: { box: "h-9", btn: "w-[34px] [&_svg]:size-4", input: "w-10 text-sm" },
};

export function NumberStepper({ label, value, onChange, min = 0, max = 99, size = "md" }:
  { label: string; value: number; onChange: (v: number) => void; min?: number; max?: number; size?: keyof typeof sizes }) {
  const s = sizes[size];
  const set = (v: number) => onChange(Math.min(max, Math.max(min, v)));
  const btn = cn(
    "grid place-items-center text-fg-muted transition hover:bg-surface-2 hover:text-fg",
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
    "disabled:pointer-events-none disabled:opacity-45",
    s.btn,
  );
  return (
    <div role="group" aria-label={label} className={cn("inline-flex shrink-0 items-stretch overflow-hidden rounded border border-border-strong bg-bg", s.box)}>
      <button type="button" aria-label={"Decrease " + label.toLowerCase()} disabled={value <= min} onClick={() => set(value - 1)} className={btn}>
        <Minus aria-hidden />
      </button>
      <input
        role="spinbutton"
        inputMode="numeric"
        aria-label={label}
        aria-valuenow={value}
        aria-valuemin={min}
        aria-valuemax={max}
        value={value}
        onChange={(e) => { const n = parseInt(e.target.value, 10); set(Number.isNaN(n) ? min : n); }}
        onKeyDown={(e) => {
          if (e.key === "ArrowUp") { e.preventDefault(); set(value + 1); }
          if (e.key === "ArrowDown") { e.preventDefault(); set(value - 1); }
        }}
        className={cn("border-x border-border bg-transparent text-center font-medium tabular-nums text-fg outline-none focus:bg-surface", s.input)}
      />
      <button type="button" aria-label={"Increase " + label.toLowerCase()} disabled={value >= max} onClick={() => set(value + 1)} className={btn}>
        <Plus aria-hidden />
      </button>
    </div>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.