Loader
A loader component that displays loading states and animations.
A loader component that displays loading states and animations.
Follow these simple steps to add the Loader component to your project:
Install Dependencies
pnpm add tailwind-merge class-variance-authority react-aria-components
Create a new file: components/ui/loader.tsx
and copy the code below:
"use client";
import { ProgressBar } from "react-aria-components";
import { twMerge } from "tailwind-merge";
import type { VariantProps } from "class-variance-authority";
import { cva } from "class-variance-authority";
function IconLoader(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="none"
viewBox="0 0 24 24"
className="size-4"
data-slot="icon"
aria-hidden="true"
{...props}
>
<path
stroke="currentColor"
stroke-opacity="0.25"
stroke-width="3.636"
d="M11.909 21a9.09 9.09 0 1 0 0-18.182 9.09 9.09 0 0 0 0 18.182Z"
></path>
<path
fill="currentColor"
d="M4.636 11.91a7.273 7.273 0 0 1 7.273-7.274V1C5.885 1 1 5.885 1 11.91zm1.819 4.81a7.24 7.24 0 0 1-1.819-4.81H1c0 2.764 1.032 5.294 2.727 7.215z"
></path>
</svg>
);
}
const loaderStyles = cva({
base: "relative",
variants: {
intent: {
current: "text-current",
primary: "text-primary",
secondary: "text-muted-fg",
success: "text-success",
warning: "text-warning",
danger: "text-danger",
},
size: {
sm: "size-4",
md: "size-6",
lg: "size-8",
xl: "size-10",
},
},
defaultVariants: {
intent: "current",
size: "sm",
},
});
type LoaderVariantProps = VariantProps<typeof loaderStyles>;
const Bars = ({ className, ...props }: React.SVGProps<SVGSVGElement>) => (
<svg
className={twMerge("size-4", className)}
data-slot="icon"
viewBox="0 0 135 140"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
{...props}
>
<rect y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.5s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0.5s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
<rect x="30" y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.25s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0.25s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
<rect x="60" width="15" height="140" rx="6">
<animate
attributeName="height"
begin="0s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
<rect x="90" y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.25s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0.25s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
<rect x="120" y="10" width="15" height="120" rx="6">
<animate
attributeName="height"
begin="0.5s"
dur="1s"
values="120;110;100;90;80;70;60;50;40;140;120"
calcMode="linear"
repeatCount="indefinite"
/>
<animate
attributeName="y"
begin="0.5s"
dur="1s"
values="10;15;20;25;30;35;40;45;50;0;10"
calcMode="linear"
repeatCount="indefinite"
/>
</rect>
</svg>
);
const Ring = (props: React.SVGProps<SVGSVGElement>) => (
<IconLoader {...props} />
);
const Spin = ({ className, ...props }: React.SVGProps<SVGSVGElement>) => (
<svg
className={twMerge("size-4", className)}
data-slot="icon"
viewBox="0 0 2400 2400"
{...props}
>
<g strokeWidth="200" strokeLinecap="round" fill="none">
<line x1="1200" y1="600" x2="1200" y2="100" />
<line opacity="0.5" x1="1200" y1="2300" x2="1200" y2="1800" />
<line opacity="0.917" x1="900" y1="680.4" x2="650" y2="247.4" />
<line opacity="0.417" x1="1750" y1="2152.6" x2="1500" y2="1719.6" />
<line opacity="0.833" x1="680.4" y1="900" x2="247.4" y2="650" />
<line opacity="0.333" x1="2152.6" y1="1750" x2="1719.6" y2="1500" />
<line opacity="0.75" x1="600" y1="1200" x2="100" y2="1200" />
<line opacity="0.25" x1="2300" y1="1200" x2="1800" y2="1200" />
<line opacity="0.667" x1="680.4" y1="1500" x2="247.4" y2="1750" />
<line opacity="0.167" x1="2152.6" y1="650" x2="1719.6" y2="900" />
<line opacity="0.583" x1="900" y1="1719.6" x2="650" y2="2152.6" />
<line opacity="0.083" x1="1750" y1="247.4" x2="1500" y2="680.4" />
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
keyTimes="0;0.08333;0.16667;0.25;0.33333;0.41667;0.5;0.58333;0.66667;0.75;0.83333;0.91667"
values="0 1199 1199;30 1199 1199;60 1199 1199;90 1199 1199;120 1199 1199;150 1199 1199;180 1199 1199;210 1199 1199;240 1199 1199;270 1199 1199;300 1199 1199;330 1199 1199"
dur="0.83333s"
begin="0.08333s"
repeatCount="indefinite"
calcMode="discrete"
/>
</g>
</svg>
);
const LOADERS = {
bars: Bars,
ring: Ring,
spin: Spin,
};
const DEFAULT_SPINNER = "spin";
interface LoaderProps
extends Omit<
React.ComponentPropsWithoutRef<"svg">,
"display" | "opacity" | "intent"
>,
LoaderVariantProps {
variant?: keyof typeof LOADERS;
percentage?: number;
isIndeterminate?: boolean;
formatOptions?: Intl.NumberFormatOptions;
ref?: React.RefObject<SVGSVGElement>;
}
const Loader = ({ isIndeterminate = true, ref, ...props }: LoaderProps) => {
const {
className,
variant = DEFAULT_SPINNER,
// intent,
// size,
...spinnerProps
} = props;
const LoaderPrimitive =
LOADERS[variant in LOADERS ? variant : DEFAULT_SPINNER];
return (
<ProgressBar
data-slot="loader"
aria-label={props["aria-label"] ?? "Loading..."}
formatOptions={props.formatOptions}
isIndeterminate={isIndeterminate}
>
<LoaderPrimitive
role="presentation"
className={loaderStyles({
// intent,
// size,
className: twMerge([
["ring"].includes(variant) && "animate-spin",
variant === "spin" && "stroke-current",
className,
]),
})}
ref={ref}
{...spinnerProps}
/>
</ProgressBar>
);
};
export { Loader };
Adjust the import paths in both files according to your project's structure.