Components
Stepper
Stepper
Interactive stepper component with progress tracking and customizable step content.
Step 1
First step
Step 2
Second step
Step 3
Third step
Step 1 content
Installation
1
Install the packages
npm i motion clsx tailwind-merge2
Add util file
lib/util.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3
Copy and paste the following code into your project
stepper.tsx
"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import React, { useState } from "react";
import { FiCheck, FiChevronRight } from "react-icons/fi";
interface Step {
id: string;
title: string;
description?: string;
content?: React.ReactNode;
}
interface StepperProps {
steps?: Step[];
currentStep?: number;
onStepChange?: (step: number) => void;
orientation?: "horizontal" | "vertical";
showContent?: boolean;
}
const Stepper: React.FC<StepperProps> = ({
steps = [
{ id: "1", title: "Step 1", description: "First step", content: <div>Step 1 content</div> },
{ id: "2", title: "Step 2", description: "Second step", content: <div>Step 2 content</div> },
{ id: "3", title: "Step 3", description: "Third step", content: <div>Step 3 content</div> },
],
currentStep = 0,
onStepChange = () => {},
orientation = "horizontal",
showContent = true
}) => {
const [activeStep, setActiveStep] = useState(currentStep);
const handleStepClick = (index: number) => {
setActiveStep(index);
onStepChange(index);
};
const isCompleted = (index: number) => index < activeStep;
const isActive = (index: number) => index === activeStep;
return (
<div className="w-full max-w-2xl p-6">
<div className={cn(
"flex",
orientation === "vertical" && "flex-col space-y-8",
orientation === "horizontal" && "justify-between"
)}>
{steps.map((step, index) => (
<React.Fragment key={step.id}>
<motion.div
className={cn(
"flex items-center",
orientation === "vertical" && "space-x-4",
orientation === "horizontal" && "flex-col items-center space-y-2 flex-1"
)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
{/* Step Circle */}
<motion.button
onClick={() => handleStepClick(index)}
className={cn(
"relative flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all",
isCompleted(index)
? "bg-green-500 border-green-500 text-white"
: isActive(index)
? "border-cyan-500 text-cyan-500"
: "border-neutral-600 text-neutral-400 hover:border-neutral-400"
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{isCompleted(index) ? (
<FiCheck className="h-5 w-5" />
) : (
<span className="text-sm font-medium">{index + 1}</span>
)}
</motion.button>
{/* Step Content */}
<div className={cn(
orientation === "vertical" && "flex-1",
orientation === "horizontal" && "text-center"
)}>
<h3 className={cn(
"font-medium",
isActive(index) ? "text-white" : "text-neutral-300"
)}>
{step.title}
</h3>
{step.description && (
<p className="text-sm text-neutral-400 mt-1">
{step.description}
</p>
)}
</div>
</motion.div>
{/* Connector Line */}
{index < steps.length - 1 && (
<motion.div
className={cn(
"flex items-center",
orientation === "horizontal" && "flex-1 mx-4",
orientation === "vertical" && "ml-6 mb-8"
)}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ delay: index * 0.1 + 0.2 }}
>
<div className={cn(
"bg-neutral-600",
orientation === "horizontal" && "h-0.5 w-full",
orientation === "vertical" && "w-0.5 h-8"
)} />
<FiChevronRight className={cn(
"text-neutral-600",
orientation === "horizontal" && "hidden",
orientation === "vertical" && "ml-2"
)} />
</motion.div>
)}
</React.Fragment>
))}
</div>
{/* Step Content */}
{showContent && steps[activeStep] && (
<motion.div
className="mt-8 p-4 bg-neutral-800 rounded-lg"
key={activeStep}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{steps[activeStep].content}
</motion.div>
)}
</div>
);
};
export default Stepper;"use client";
import { cn } from "@/lib/utils";
import { motion } from "motion/react";
import React, { useState } from "react";
import { FiCheck, FiChevronRight } from "react-icons/fi";
interface Step {
id: string;
title: string;
description?: string;
content?: React.ReactNode;
}
interface StepperProps {
steps?: Step[];
currentStep?: number;
onStepChange?: (step: number) => void;
orientation?: "horizontal" | "vertical";
showContent?: boolean;
}
const Stepper: React.FC<StepperProps> = ({
steps = [
{ id: "1", title: "Step 1", description: "First step", content: <div>Step 1 content</div> },
{ id: "2", title: "Step 2", description: "Second step", content: <div>Step 2 content</div> },
{ id: "3", title: "Step 3", description: "Third step", content: <div>Step 3 content</div> },
],
currentStep = 0,
onStepChange = () => {},
orientation = "horizontal",
showContent = true
}) => {
const [activeStep, setActiveStep] = useState(currentStep);
const handleStepClick = (index: number) => {
setActiveStep(index);
onStepChange(index);
};
const isCompleted = (index: number) => index < activeStep;
const isActive = (index: number) => index === activeStep;
return (
<div className="w-full max-w-2xl p-6">
<div className={cn(
"flex",
orientation === "vertical" && "flex-col space-y-8",
orientation === "horizontal" && "justify-between"
)}>
{steps.map((step, index) => (
<React.Fragment key={step.id}>
<motion.div
className={cn(
"flex items-center",
orientation === "vertical" && "space-x-4",
orientation === "horizontal" && "flex-col items-center space-y-2 flex-1"
)}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
{/* Step Circle */}
<motion.button
onClick={() => handleStepClick(index)}
className={cn(
"relative flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all",
isCompleted(index)
? "bg-green-500 border-green-500 text-white"
: isActive(index)
? "border-cyan-500 text-cyan-500"
: "border-neutral-600 text-neutral-400 hover:border-neutral-400"
)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{isCompleted(index) ? (
<FiCheck className="h-5 w-5" />
) : (
<span className="text-sm font-medium">{index + 1}</span>
)}
</motion.button>
{/* Step Content */}
<div className={cn(
orientation === "vertical" && "flex-1",
orientation === "horizontal" && "text-center"
)}>
<h3 className={cn(
"font-medium",
isActive(index) ? "text-white" : "text-neutral-300"
)}>
{step.title}
</h3>
{step.description && (
<p className="text-sm text-neutral-400 mt-1">
{step.description}
</p>
)}
</div>
</motion.div>
{/* Connector Line */}
{index < steps.length - 1 && (
<motion.div
className={cn(
"flex items-center",
orientation === "horizontal" && "flex-1 mx-4",
orientation === "vertical" && "ml-6 mb-8"
)}
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ delay: index * 0.1 + 0.2 }}
>
<div className={cn(
"bg-neutral-600",
orientation === "horizontal" && "h-0.5 w-full",
orientation === "vertical" && "w-0.5 h-8"
)} />
<FiChevronRight className={cn(
"text-neutral-600",
orientation === "horizontal" && "hidden",
orientation === "vertical" && "ml-2"
)} />
</motion.div>
)}
</React.Fragment>
))}
</div>
{/* Step Content */}
{showContent && steps[activeStep] && (
<motion.div
className="mt-8 p-4 bg-neutral-800 rounded-lg"
key={activeStep}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
{steps[activeStep].content}
</motion.div>
)}
</div>
);
};
export default Stepper;4
Update the import paths to match your project setup
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| steps | Step[] | [] | Array of step objects with id, title, description, and content. |
| currentStep | number | 0 | Current active step index. |
| onStepChange | function | () => {} | Callback when step changes. |
| orientation | string | horizontal | Stepper orientation (horizontal or vertical). |
| showContent | boolean | true | Show step content below the stepper. |