Stepper with numbers only
Installation
Follow these simple steps to add the Select component to your project:
Install Dependencies
pnpm add @radix-ui/react-slot
Create a new file components/ui/stepper.tsx
and copy the code below:
"use client";
import { cn } from "@/lib/utils";
import { Slot } from "@radix-ui/react-slot";
import { CheckIcon, LoaderCircleIcon } from "lucide-react";
import * as React from "react";
import { createContext, useContext } from "react";
// Types
type StepperContextValue = {
activeStep: number;
setActiveStep: (step: number) => void;
orientation: "horizontal" | "vertical";
};
type StepItemContextValue = {
step: number;
state: StepState;
isDisabled: boolean;
isLoading: boolean;
};
type StepState = "active" | "completed" | "inactive" | "loading";
// Contexts
const StepperContext = createContext<StepperContextValue | undefined>(undefined);
const StepItemContext = createContext<StepItemContextValue | undefined>(undefined);
const useStepper = () => {
const context = useContext(StepperContext);
if (!context) {
throw new Error("useStepper must be used within a Stepper");
}
return context;
};
const useStepItem = () => {
const context = useContext(StepItemContext);
if (!context) {
throw new Error("useStepItem must be used within a StepperItem");
}
return context;
};
// Components
interface StepperProps extends React.HTMLAttributes<HTMLDivElement> {
defaultValue?: number;
value?: number;
onValueChange?: (value: number) => void;
orientation?: "horizontal" | "vertical";
}
function Stepper({
defaultValue = 0,
value,
onValueChange,
orientation = "horizontal",
className,
...props
}: StepperProps) {
const [activeStep, setInternalStep] = React.useState(defaultValue);
const setActiveStep = React.useCallback(
(step: number) => {
if (value === undefined) {
setInternalStep(step);
}
onValueChange?.(step);
},
[value, onValueChange],
);
const currentStep = value ?? activeStep;
return (
<StepperContext.Provider
value={{
activeStep: currentStep,
setActiveStep,
orientation,
}}
>
<div
data-slot="stepper"
className={cn(
"group/stepper inline-flex data-[orientation=horizontal]:w-full data-[orientation=horizontal]:flex-row data-[orientation=vertical]:flex-col",
className,
)}
data-orientation={orientation}
{...props}
/>
</StepperContext.Provider>
);
}
// StepperItem
interface StepperItemProps extends React.HTMLAttributes<HTMLDivElement> {
step: number;
completed?: boolean;
disabled?: boolean;
loading?: boolean;
}
function StepperItem({
step,
completed = false,
disabled = false,
loading = false,
className,
children,
...props
}: StepperItemProps) {
const { activeStep } = useStepper();
const state: StepState =
completed || step < activeStep ? "completed" : activeStep === step ? "active" : "inactive";
const isLoading = loading && step === activeStep;
return (
<StepItemContext.Provider value={{ step, state, isDisabled: disabled, isLoading }}>
<div
data-slot="stepper-item"
className={cn(
"group/step flex items-center group-data-[orientation=horizontal]/stepper:flex-row group-data-[orientation=vertical]/stepper:flex-col",
className,
)}
data-state={state}
{...(isLoading ? { "data-loading": true } : {})}
{...props}
>
{children}
</div>
</StepItemContext.Provider>
);
}
// StepperTrigger
interface StepperTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
}
function StepperTrigger({ asChild = false, className, children, ...props }: StepperTriggerProps) {
const { setActiveStep } = useStepper();
const { step, isDisabled } = useStepItem();
if (asChild) {
const Comp = asChild ? Slot : "span";
return (
<Comp data-slot="stepper-trigger" className={className}>
{children}
</Comp>
);
}
return (
<button
data-slot="stepper-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 inline-flex items-center gap-3 rounded-full outline-none focus-visible:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50",
className,
)}
onClick={() => setActiveStep(step)}
disabled={isDisabled}
{...props}
>
{children}
</button>
);
}
// StepperIndicator
interface StepperIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {
asChild?: boolean;
}
function StepperIndicator({
asChild = false,
className,
children,
...props
}: StepperIndicatorProps) {
const { state, step, isLoading } = useStepItem();
return (
<span
data-slot="stepper-indicator"
className={cn(
"bg-muted text-muted-foreground data-[state=active]:bg-primary data-[state=completed]:bg-primary data-[state=active]:text-primary-foreground data-[state=completed]:text-primary-foreground relative flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-medium",
className,
)}
data-state={state}
{...props}
>
{asChild ? (
children
) : (
<>
<span className="transition-all group-data-loading/step:scale-0 group-data-loading/step:opacity-0 group-data-loading/step:transition-none group-data-[state=completed]/step:scale-0 group-data-[state=completed]/step:opacity-0">
{step}
</span>
<CheckIcon
className="absolute scale-0 opacity-0 transition-all group-data-[state=completed]/step:scale-100 group-data-[state=completed]/step:opacity-100"
size={16}
aria-hidden="true"
/>
{isLoading && (
<span className="absolute transition-all">
<LoaderCircleIcon className="animate-spin" size={14} aria-hidden="true" />
</span>
)}
</>
)}
</span>
);
}
// StepperTitle
function StepperTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
return (
<h3 data-slot="stepper-title" className={cn("text-sm font-medium", className)} {...props} />
);
}
// StepperDescription
function StepperDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
return (
<p
data-slot="stepper-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
// StepperSeparator
function StepperSeparator({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
data-slot="stepper-separator"
className={cn(
"bg-muted group-data-[state=completed]/step:bg-primary m-0.5 group-data-[orientation=horizontal]/stepper:h-0.5 group-data-[orientation=horizontal]/stepper:w-full group-data-[orientation=horizontal]/stepper:flex-1 group-data-[orientation=vertical]/stepper:h-12 group-data-[orientation=vertical]/stepper:w-0.5",
className,
)}
{...props}
/>
);
}
export {
Stepper,
StepperDescription,
StepperIndicator,
StepperItem,
StepperSeparator,
StepperTitle,
StepperTrigger,
};
Adjust the import paths in both files according to your project's structure.
Examples
W/ next & previous steps
1
2
3
4
Controlled stepper with checkmarks
Vertical orientation
1
2
3
4
Controlled vertical stepper with checkmarks