Search

Ctrl + K

Steps

This document outlines the steps to create Steps component styled withTailwind CSS and using some npm dependency libraries.

Prerequisites

  • 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.
Step 1: Create Circle component
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
Step 2: Create Info component
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
Step 3: Create Line component
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
Step 4: Create Steps component
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