Componentform
Stepper
Increment and decrement buttons with mono readout.
Preview
Live Preview
2
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add stepperOr install the full npm package:
npm install bigbulluiUsage
import { Stepper } from "@/components/ui/stepper";
<Stepper defaultValue={1} min={0} max={10} />Source code
stepper.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface StepperProps {
value?: number;
defaultValue?: number;
min?: number;
max?: number;
step?: number;
onValueChange?: (value: number) => void;
disabled?: boolean;
className?: string;
"aria-label"?: string;
}
export function Stepper({
value: controlledValue,
defaultValue = 0,
min = 0,
max = 100,
step = 1,
onValueChange,
disabled = false,
className,
"aria-label": ariaLabel = "Stepper",
}: StepperProps) {
const [uncontrolledValue, setUncontrolledValue] = React.useState(defaultValue);
const isControlled = controlledValue !== undefined;
const currentVal = isControlled ? controlledValue : uncontrolledValue;
const clamp = React.useCallback(
(val: number) => Math.min(Math.max(val, min), max),
[min, max]
);
const updateValue = (nextVal: number) => {
const clamped = clamp(nextVal);
if (!isControlled) {
setUncontrolledValue(clamped);
}
onValueChange?.(clamped);
};
const handleDecrement = () => {
if (disabled || currentVal <= min) return;
updateValue(currentVal - step);
};
const handleIncrement = () => {
if (disabled || currentVal >= max) return;
updateValue(currentVal + step);
};
const isMin = currentVal <= min;
const isMax = currentVal >= max;
return (
<div
role="group"
aria-label={ariaLabel}
className={cn(
"inline-flex items-center rounded-md border-2 border-dashed border-border bg-card p-1",
disabled && "opacity-50",
className
)}
>
<button
type="button"
disabled={disabled || isMin}
onClick={handleDecrement}
aria-label="Decrease value"
className={cn(
"flex size-8 items-center justify-center rounded-sm border border-border bg-secondary font-mono text-sm font-bold transition-all",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
isMin || disabled
? "cursor-not-allowed opacity-40"
: "cursor-pointer hover:bg-primary hover:text-primary-foreground active:scale-95"
)}
>
−
</button>
<span
aria-live="polite"
className="min-w-12 px-3 text-center font-mono text-sm font-semibold tracking-wider text-foreground"
>
{currentVal}
</span>
<button
type="button"
disabled={disabled || isMax}
onClick={handleIncrement}
aria-label="Increase value"
className={cn(
"flex size-8 items-center justify-center rounded-sm border border-border bg-secondary font-mono text-sm font-bold transition-all",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
isMax || disabled
? "cursor-not-allowed opacity-40"
: "cursor-pointer hover:bg-primary hover:text-primary-foreground active:scale-95"
)}
>
+
</button>
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| value | number | Controlled value. |
| defaultValue | number | Initial value (default 0). |
| min | number | Minimum allowed value (default 0). |
| max | number | Maximum allowed value (default 100). |
| step | number | Step increment (default 1). |
| onValueChange | (value: number) => void | Callback when value changes. |
| disabled | boolean | Disables stepper interaction. |