This document outlines the steps to create Spinner
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// tailwind.config.js
2module.exports = {
3 ...
4 theme: {
5 ...
6 extend: {
7 ...
8 keyframes: {
9 pulse: {
10 "0%": {
11 transform: "scale(0)",
12 opacity: "0.5",
13 },
14 "50%": {
15 transform: "scale(1)",
16 opacity: "1",
17 },
18 "100%": {
19 transform: "scale(0)",
20 opacity: "0.5",
21 },
22 },
23 bounce: {
24 from: { transform: "translateY(-10px)" },
25 to: { transform: "translateY(10px)" },
26 },
27 fade: {
28 from: { opacity: "1" },
29 to: { opacity: "0" },
30 },
31 scale: {
32 "0%": {
33 transform: "scaleY(0.4)",
34 },
35 "20%": {
36 transform: "scaleY(1)",
37 },
38 "40%": {
39 transform: "scaleY(0.4)",
40 },
41 "100%": {
42 transform: "scaleY(0.4)",
43 },
44 },
45 "clip-loader": {
46 "100%": {
47 transform: "rotate(360deg)",
48 },
49 },
50 "clip-circle": {
51 "0%": {
52 strokeDasharray: "1,200",
53 strokeDashoffset: "0",
54 },
55 "50%": {
56 strokeDasharray: "90,200",
57 strokeDashoffset: "-35px",
58 },
59 "100%": {
60 strokeDashoffset: "-125px",
61 },
62 },
63 },
64 animation: {
65 pulse: "pulse 1.111s ease-in-out infinite",
66 bounce: "bounce 0.5s ease-in infinite alternate",
67 fade: "fade 1s infinite linear",
68 scale: "scale 0.9s ease-in-out infinite",
69 "clip-loader": "clip-loader 2s linear infinite",
70 "clip-circle": "clip-circle 1.5s ease-in-out infinite",
71 },
72 },
73 },
74 plugins: [require("tailwindcss-animate")]
75};
76
1// bounce.tsx
2import clsx from "clsx";
3import { cva, type VariantProps } from "class-variance-authority";
4
5const sizeVariants = cva("grid grid-cols-4", {
6 variants: {
7 size: {
8 small: "w-[2.25rem] h-6 gap-1",
9 medium: "w-[2.875rem] h-8 gap-1.5",
10 large: "w-[4.5rem] h-12 gap-2",
11 },
12 },
13});
14
15interface Props extends VariantProps<typeof sizeVariants> {
16 color: string;
17 className?: string;
18}
19
20const Bounce: React.FC<Props> = ({ color, size, className }) => {
21 return (
22 <div className={clsx(sizeVariants({ size }), className)}>
23 {new Array(4).fill(null).map((_, index) => (
24 <div
25 key={index}
26 style={{
27 backgroundColor: color,
28 animationDelay: `${index * 0.16}s`,
29 }}
30 className="my-auto aspect-square flex-shrink-0 animate-bounce rounded-full first-letter:flex-grow-0"
31 />
32 ))}
33 </div>
34 );
35};
36
37export default Bounce;
38
1// clip.tsx
2import clsx from "clsx";
3import { cva, type VariantProps } from "class-variance-authority";
4
5const sizeVariants = cva("animate-clip-loader", {
6 variants: {
7 size: {
8 small: "w-6 h-6",
9 medium: "w-8 h-8",
10 large: "w-12 h-12",
11 },
12 },
13});
14
15interface Props extends VariantProps<typeof sizeVariants> {
16 color: string;
17 className?: string;
18}
19
20const Clip: React.FC<Props> = ({ color, size, className }) => {
21 return (
22 <svg
23 viewBox="25 25 50 50"
24 className={clsx(sizeVariants({ size }), className)}
25 >
26 <circle
27 r="20"
28 cy="50"
29 cx="50"
30 fill="none"
31 stroke={color}
32 strokeWidth={4}
33 strokeDasharray="1,200"
34 strokeDashoffset={0}
35 strokeLinecap="round"
36 className="animate-clip-circle"
37 ></circle>
38 </svg>
39 );
40};
41
42export default Clip;
43
1// fade.tsx
2import clsx from "clsx";
3import { cva, type VariantProps } from "class-variance-authority";
4
5const sizeVariants = cva("relative inline-block w-[1em] h-[1em]", {
6 variants: {
7 size: {
8 small: "text-[1.5rem]",
9 medium: "text-[2rem]",
10 large: "text-[3rem]",
11 },
12 },
13});
14
15interface Props extends VariantProps<typeof sizeVariants> {
16 color: string;
17 className?: string;
18}
19
20const Fade: React.FC<Props> = ({ color, size, className }) => {
21 return (
22 <div className={clsx(sizeVariants({ size }), className)}>
23 {new Array(12).fill(null).map((_, index) => (
24 <div
25 key={index}
26 style={{
27 backgroundColor: color,
28 animationDelay: `${index * 0.083}s`,
29 transform: `rotate(${index * 30}deg)`,
30 }}
31 className="animate-fade absolute bottom-0 left-[0.4629em] h-[0.2777em] w-[0.074em] origin-[center_-0.2222em] rounded-[0.0555em]"
32 />
33 ))}
34 </div>
35 );
36};
37
38export default Fade;
39
1// pulse.tsx
2import clsx from "clsx";
3import { cva, type VariantProps } from "class-variance-authority";
4
5const sizeVariants = cva("relative flex items-center", {
6 variants: {
7 size: {
8 small: "w-6 h-6",
9 medium: "w-8 h-8",
10 large: "w-12 h-12",
11 },
12 },
13});
14
15interface Props extends VariantProps<typeof sizeVariants> {
16 color: string;
17 className?: string;
18}
19
20const Pulse: React.FC<Props> = ({ color, size, className }) => {
21 return (
22 <div className={clsx(sizeVariants({ size }), className)}>
23 {new Array(8).fill(null).map((_, index) => (
24 <div
25 key={index}
26 style={{ transform: `rotate(${index * 45}deg)` }}
27 className="absolute left-0 top-0 flex h-full w-full items-center"
28 >
29 <div
30 style={{
31 backgroundColor: color,
32 animationDelay: `${(8 - index) * 0.125}s`,
33 }}
34 className="h-1/5 w-1/5 scale-0 animate-pulse rounded-full opacity-50"
35 />
36 </div>
37 ))}
38 </div>
39 );
40};
41
42export default Pulse;
43
1// scale.tsx
2import clsx from "clsx";
3import { cva, type VariantProps } from "class-variance-authority";
4
5const sizeVariants = cva("flex justify-center items-center gap-1.5", {
6 variants: {
7 size: {
8 small: "w-6 h-6",
9 medium: "w-8 h-8",
10 large: "w-12 h-12",
11 },
12 },
13});
14
15const barVariants = cva("flex-shrink-0 h-full animate-scale", {
16 variants: {
17 size: {
18 small: "w-[2px]",
19 medium: "w-[3px]",
20 large: "w-[4px]",
21 },
22 },
23});
24
25interface Props extends VariantProps<typeof sizeVariants> {
26 color: string;
27 className?: string;
28}
29
30const Scale: React.FC<Props> = ({ color, size, className }) => {
31 return (
32 <div className={clsx(sizeVariants({ size }), className)}>
33 {new Array(5).fill(null).map((_, index) => (
34 <div
35 key={index}
36 style={{
37 backgroundColor: color,
38 animationDelay: `-${index ? 0.9 - index * 0.1 : 0}s`,
39 transform: `rotate(${index * 30}deg)`,
40 }}
41 className={barVariants({ size })}
42 />
43 ))}
44 </div>
45 );
46};
47
48export default Scale;
49
1// spinner.component.tsx
2import { cva, type VariantProps } from "class-variance-authority";
3import Bounce from "./bounce";
4import Clip from "./clip";
5import Fade from "./fade";
6import Pulse from "./pulse";
7import Scale from "./scale";
8
9const colorVariants = cva("", {
10 variants: {
11 color: {
12 primary: "#3b82f6",
13 secondary: "#6b7280",
14 success: "#22c55e",
15 danger: "#ef4444",
16 warning: "#eab308",
17 info: "#06b6d4",
18 light: "#d1d5db",
19 dark: "#000000",
20 },
21 },
22});
23
24export interface SpinnerProps extends VariantProps<typeof colorVariants> {
25 variant?: "clip" | "fade" | "scale" | "bounce" | "pulse";
26 size?: "small" | "medium" | "large";
27 className?: string;
28}
29
30const Spinner: React.FC<SpinnerProps> = ({
31 variant = "clip",
32 color = "primary",
33 size = "medium",
34 className,
35}) => {
36 const colorValue = colorVariants({ color });
37
38 return variant === "clip" ? (
39 <Clip size={size} color={colorValue} className={className} />
40 ) : variant === "fade" ? (
41 <Fade size={size} color={colorValue} className={className} />
42 ) : variant === "scale" ? (
43 <Scale size={size} color={colorValue} className={className} />
44 ) : variant === "bounce" ? (
45 <Bounce size={size} color={colorValue} className={className} />
46 ) : variant === "pulse" ? (
47 <Pulse size={size} color={colorValue} className={className} />
48 ) : null;
49};
50
51export default Spinner;
52