This document outlines the steps to create Steps
component styled withTailwind CSS and using some npm dependency libraries.
Node.js
and npm
installed on your machine.Tailwind CSS
installed in your project.CVA(class-variance-authority)
is a utility for managing CSS class names based on various conditions.clsx
is a tiny utility for constructing className strings conditionally.1// circle.tsx
2import clsx from "clsx";
3import { cva, VariantProps } from "class-variance-authority";
4import { CheckIcon } from "lucide-react";
5
6const variants = cva(
7 "flex justify-center items-center w-12 h-12 rounded-full text-lg font-bold",
8 {
9 variants: {
10 color: {
11 primary: [
12 "data-[completed=true]:bg-blue-500 data-[completed=true]:text-white",
13 "data-[current=true]:border-blue-500 data-[current=true]:bg-blue-200 data-[current=true]:text-blue-700",
14 ],
15 secondary: [
16 "data-[completed=true]:bg-neutral-500 data-[completed=true]:text-white",
17 "data-[current=true]:border-neutral-500 data-[current=true]:bg-neutral-200 data-[current=true]:text-neutral-700",
18 ],
19 success: [
20 "data-[completed=true]:bg-green-500 data-[completed=true]:text-white",
21 "data-[current=true]:border-green-500 data-[current=true]:bg-green-200 data-[current=true]:text-green-700",
22 ],
23 danger: [
24 "data-[completed=true]:bg-red-500 data-[completed=true]:text-white",
25 "data-[current=true]:border-red-500 data-[current=true]:bg-red-200 data-[current=true]:text-red-700",
26 ],
27 warning: [
28 "data-[completed=true]:bg-yellow-500 data-[completed=true]:text-white",
29 "data-[current=true]:border-yellow-500 data-[current=true]:bg-yellow-200 data-[current=true]:text-yellow-700",
30 ],
31 info: [
32 "data-[completed=true]:bg-cyan-500 data-[completed=true]:text-white",
33 "data-[current=true]:border-cyan-500 data-[current=true]:bg-cyan-200 data-[current=true]:text-cyan-700",
34 ],
35 light: [
36 "data-[completed=true]:bg-neutral-300 data-[completed=true]:text-white",
37 "data-[current=true]:border-neutral-300 data-[current=true]:bg-neutral-100 data-[current=true]:text-neutral-400",
38 ],
39 dark: [
40 "data-[completed=true]:bg-neutral-900 data-[completed=true]:text-white",
41 "data-[current=true]:border-neutral-900 data-[current=true]:bg-neutral-200 data-[current=true]:text-neutral-700",
42 ],
43 },
44 },
45 },
46);
47
48export type Color = VariantProps<typeof variants>["color"];
49
50interface Props {
51 index: number;
52 icon?: React.ReactNode;
53 variant: "upcoming" | "current" | "completed";
54 color: Color;
55}
56
57const Circle: React.FC<Props> = ({ index, icon, variant, color }) => {
58 return (
59 <div
60 data-completed={variant === "completed"}
61 data-current={variant === "current"}
62 className={clsx(variants({ color }), {
63 "border-2": variant !== "completed",
64 })}
65 >
66 {variant === "completed" ? (
67 <CheckIcon width={16} height={16} />
68 ) : (
69 icon || <span>{index + 1}</span>
70 )}
71 </div>
72 );
73};
74
75export default Circle;
76
1// info.tsx
2interface Props {
3 title?: React.ReactNode;
4 description?: React.ReactNode;
5 className?: string;
6}
7
8const Info: React.FC<Props> = ({ title, description, className }) => {
9 return (
10 <div className={className}>
11 {title && <div className="font-semibold">{title}</div>}
12 {description && (
13 <div className="text-sm text-neutral-500">{description}</div>
14 )}
15 </div>
16 );
17};
18
19export default Info;
20
1// line.tsx
2import clsx from "clsx";
3import { cva, VariantProps } from "class-variance-authority";
4
5const variants = cva(
6 "h-0.5 mx-2 data-[active=false]:bg-neutral-300 dark:data-[active=false]:bg-neutral-600",
7 {
8 variants: {
9 color: {
10 primary: "data-[active=true]:bg-blue-500",
11 secondary: "data-[active=true]:bg-neutral-500",
12 success: "data-[active=true]:bg-green-500",
13 danger: "data-[active=true]:bg-red-500",
14 warning: "data-[active=true]:bg-yellow-500",
15 info: "data-[active=true]:bg-cyan-500",
16 light: "data-[active=true]:bg-neutral-400",
17 dark: "data-[active=true]:bg-neutral-900",
18 },
19 },
20 },
21);
22
23interface Props extends VariantProps<typeof variants> {
24 display: "active" | "inactive" | "hidden";
25}
26
27const Line: React.FC<Props> = ({ display, color }) => {
28 return (
29 <div
30 data-active={display === "active"}
31 className={clsx(
32 variants({ color }),
33 display === "hidden" ? "flex-[1] opacity-0" : "flex-[2]",
34 )}
35 />
36 );
37};
38
39export default Line;
40
1// steps.component.tsx
2import clsx from "clsx";
3import { Fragment } from "react";
4import Circle, { Color } from "./circle";
5import Info from "./info";
6import Line from "./line";
7
8export interface Step {
9 title?: React.ReactNode;
10 description?: React.ReactNode;
11 icon?: React.ReactNode;
12}
13
14export interface StepsProps {
15 current: number;
16 steps: Step[];
17 color?: Color;
18 position?: "inline" | "break-line";
19 className?: string;
20}
21
22const Steps: React.FC<StepsProps> = ({
23 current,
24 steps,
25 color = "primary",
26 position = "inline",
27 className,
28}) => {
29 return (
30 <div className={clsx("flex items-center", className)}>
31 {position === "inline" ? (
32 steps.map((step, index) => (
33 <Fragment key={index}>
34 {index !== 0 && (
35 <Line
36 color={color}
37 display={current >= index + 1 ? "active" : "inactive"}
38 />
39 )}
40 <div className="flex items-center">
41 <Circle
42 color={color}
43 index={index}
44 icon={step.icon}
45 variant={
46 current === index + 1
47 ? "current"
48 : current > index + 1
49 ? "completed"
50 : "upcoming"
51 }
52 />
53 {(step.title || step.description) && (
54 <Info
55 title={step.title}
56 description={step.description}
57 className="ml-4"
58 />
59 )}
60 </div>
61 </Fragment>
62 ))
63 ) : (
64 <div className="w-full">
65 <div className="flex w-full items-center">
66 <Line display="hidden" />
67 {steps.map((step, index) => (
68 <Fragment key={index}>
69 {index !== 0 && (
70 <Line
71 color={color}
72 display={current >= index + 1 ? "active" : "inactive"}
73 />
74 )}
75 <Circle
76 color={color}
77 index={index}
78 icon={step.icon}
79 variant={
80 current === index + 1
81 ? "current"
82 : current > index + 1
83 ? "completed"
84 : "upcoming"
85 }
86 />
87 </Fragment>
88 ))}
89 <Line display="hidden" />
90 </div>
91 <div className="flex">
92 {steps.map((step, index) => (
93 <div
94 key={index}
95 className="flex flex-1 flex-col items-center px-4"
96 >
97 {(step.title || step.description) && (
98 <Info
99 title={step.title}
100 description={step.description}
101 className="mt-2 flex flex-col items-center"
102 />
103 )}
104 </div>
105 ))}
106 </div>
107 </div>
108 )}
109 </div>
110 );
111};
112
113export default Steps;
114