Step By Step
Numbered step guide with optional interactive completion tracking.
Getting Started
1
Install
Run npm install to install dependencies.
2
Configure
Set up your configuration files.
3
Build
Build your application for production.
Installation
pnpm dlx shadcn@latest add https://ui.vllnt.com/r/step-by-step.jsonbash
Code
"use client";
import { useState } from "react";
import type { ReactNode } from "react";
import { cn } from "../../lib/utils";
export type StepProps = {
children: ReactNode;
className?: string;
number?: number;
title: string;
};
function Step({
children,
className,
number,
title,
}: StepProps): React.ReactNode {
return (
<div className={cn("flex gap-4", className)}>
<div className="flex flex-col items-center">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-bold">
{number}
</div>
<div className="w-px flex-1 bg-border mt-2" />
</div>
<div className="flex-1 pb-8 last:pb-0">
<h4 className="font-semibold text-foreground mb-2">{title}</h4>
<div className="text-sm text-muted-foreground [&>p]:mb-2 [&>pre]:my-2">
{children}
</div>
</div>
</div>
);
}
type InteractiveStepProps = {
children: ReactNode;
isCompleted: boolean;
isLast: boolean;
onToggle: () => void;
stepNumber: number;
title: string;
};
function InteractiveStep({
children,
isCompleted,
isLast,
onToggle,
stepNumber,
title,
}: InteractiveStepProps): React.ReactNode {
return (
<div className="flex gap-4">
<div className="flex flex-col items-center">
<button
className={cn(
"flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold transition-colors",
isCompleted
? "bg-green-500 text-white"
: "bg-primary text-primary-foreground",
)}
onClick={onToggle}
type="button"
>
{isCompleted ? (
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="M5 13l4 4L19 7"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
) : (
stepNumber
)}
</button>
{!isLast && (
<div
className={cn(
"w-px flex-1 mt-2",
isCompleted ? "bg-green-500" : "bg-border",
)}
/>
)}
</div>
<div
className={cn(
"flex-1 pb-8 transition-opacity",
isCompleted && "opacity-60",
)}
>
<h4
className={cn(
"font-semibold text-foreground mb-2",
isCompleted && "line-through",
)}
>
{title}
</h4>
<div className="text-sm text-muted-foreground [&>p]:mb-2 [&>pre]:my-2">
{children}
</div>
</div>
</div>
);
}
export type StepByStepProps = {
children: ReactNode;
className?: string;
interactive?: boolean;
title?: string;
};
// eslint-disable-next-line max-lines-per-function -- Complex component with interactive/non-interactive modes
function StepByStep({
children,
className,
interactive = false,
title,
}: StepByStepProps): React.ReactNode {
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());
const steps = Array.isArray(children) ? children : [children];
const toggleStep = (index: number): void => {
const newCompleted = new Set(completedSteps);
if (newCompleted.has(index)) newCompleted.delete(index);
else newCompleted.add(index);
setCompletedSteps(newCompleted);
};
if (!interactive) {
return (
<div className={cn("my-6", className)}>
{title ? (
<div className="flex items-center gap-2 mb-4">
<svg
className="h-5 w-5 text-primary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="m9 18 6-6-6-6"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
<h3 className="font-semibold text-lg">{title}</h3>
</div>
) : null}
<div className="space-y-0">
{steps.map((step, index) => {
const stepElement = step as React.ReactElement<StepProps>;
return (
<Step
key={index}
number={index + 1}
title={stepElement.props.title}
>
{stepElement.props.children}
</Step>
);
})}
</div>
</div>
);
}
return (
<div className={cn("my-6", className)}>
{title ? (
<div className="flex items-center gap-2 mb-4">
<svg
className="h-5 w-5 text-primary"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
d="m9 18 6-6-6-6"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
/>
</svg>
<h3 className="font-semibold text-lg">{title}</h3>
<span className="text-xs text-muted-foreground ml-auto">
{completedSteps.size}/{steps.length} completed
</span>
</div>
) : null}
<div className="space-y-0">
{steps.map((step, index) => (
<InteractiveStep
isCompleted={completedSteps.has(index)}
isLast={index === steps.length - 1}
key={index}
onToggle={() => {
toggleStep(index);
}}
stepNumber={index + 1}
title={(step as React.ReactElement<StepProps>).props.title}
>
{(step as React.ReactElement<StepProps>).props.children}
</InteractiveStep>
))}
</div>
</div>
);
}
StepByStep.Step = Step;
export { Step, StepByStep };
typescript