Back to all components
Loading States
Various loading animations and indicators
1

Installation

Use the following command to install the component:

npx shadcn@latest add https://ui.sanjid.shop/r/loader.json
pnpm dlx shadcn@latest add https://ui.sanjid.shop/r/loader.json
yarn shadcn@latest add https://ui.sanjid.shop/r/loader.json
bunx --bun shadcn@latest add https://ui.sanjid.shop/r/loader.json

Install the following dependencies:

npm install motion clsx tailwind-merge
pnpm add motion clsx tailwind-merge
yarn add motion clsx tailwind-merge
bun add motion clsx tailwind-merge

Add util file

lib/utils.ts
1import { ClassValue, clsx } from "clsx";2import { twMerge } from "tailwind-merge";3
4export function cn(...inputs: ClassValue[]) {5  return twMerge(clsx(inputs));6}

Copy and paste the following code into your project.

components/ui/loader.tsx
1"use client";2
3import { motion } from "motion/react";4import clsx from "clsx";5
6type LoaderVariant = "spinner" | "pulse" | "bounce";7type LoaderSize = "xs" | "sm" | "md" | "lg" | "xl";8
9const sizeMap: Record<LoaderSize, number> = {10  xs: 8,11  sm: 12,12  md: 16,13  lg: 24,14  xl: 32,15};16
17interface LoaderProps {18  variant?: LoaderVariant;19  size?: LoaderSize;20  color?: string;21  className?: string;22}23
24export function Loader({25  variant = "spinner",26  size = "md",27  color = "bg-primary",28  className,29}: LoaderProps) {30  const numericSize = sizeMap[size];31
32  switch (variant) {33    case "pulse":34      return (35        <div className={clsx("flex gap-2", className)}>36          {[0, 1, 2].map((i) => (37            <motion.div38              key={i}39              className={clsx(`rounded-full ${color}`)}40              style={{ width: numericSize, height: numericSize }}41              animate={{ scale: [1, 1.5, 1], opacity: [1, 0.5, 1] }}42              transition={{ duration: 1, repeat: Infinity, delay: i * 0.2 }}43            />44          ))}45        </div>46      );47
48    case "bounce":49      return (50        <div className={clsx("flex gap-2", className)}>51          {[0, 1, 2].map((i) => (52            <motion.div53              key={i}54              className={clsx(`rounded-full ${color}`)}55              style={{ width: numericSize, height: numericSize }}56              animate={{ y: [0, -20, 0] }}57              transition={{ duration: 1, repeat: Infinity, delay: i * 0.2 }}58            />59          ))}60        </div>61      );62
63    case "spinner":64    default:65      return (66        <motion.div67          className={clsx(68            `rounded-full border-4 border-t-transparent bg-transparent ${color}`,69            className,70          )}71          style={{72            width: numericSize * 3,73            height: numericSize * 3,74          }}75          animate={{ rotate: 360 }}76          transition={{ duration: 1, repeat: Infinity, ease: "linear" }}77        />78      );79  }80}

Update the import paths to match your project setup.

Basic Usage

1import { Loader } from "@/components/ui/loader";2
3function MyComponent() {4  return (5    <div>6      <Loader variant="spinner" size="md" />7    </div>8  );9}

Variants

A classic spinning loader with customizable colors.

1<Loader variant="spinner" />

Bouncing dots that animate in a wave pattern.

1<Loader variant="bounce" />

A pulsing circle with fade effects.

1<Loader variant="pulse" />

A morphing loader that changes shape and color.

1<Loader variant="morph" />

Sizes

Loaders come in four sizes:

1<Loader size="sm" />   // Small2<Loader size="md" />   // Medium (default)3<Loader size="lg" />   // Large4<Loader size="xl" />   // Extra large

Props

PropTypeRequiredDefaultDescription
variantstringNoThe type of loading animation
sizestringNoThe size of the loader
colorstringNoThe color of the loader
speedstringNoAnimation speed
classNamestringNoAdditional CSS classes

Examples

1function LoadingButton() {2  const [isLoading, setIsLoading] = useState(false);3
4  return (5    <Button onClick={() => setIsLoading(true)} disabled={isLoading}>6      {isLoading ? (7        <>8          <Loader size="sm" className="mr-2" />9          Loading...10        </>11      ) : (12        "Submit"13      )}14    </Button>15  );16}

1

Best Practices

  1. Appropriate Size: Use smaller loaders for buttons, larger for page loads
  2. Consistent Placement: Keep loaders in consistent locations across your app
  3. Meaningful Text: Include descriptive text with loaders when appropriate
  4. Accessibility: Always provide ARIA labels for screen readers
  5. Performance: Use lightweight animations for better performance

Accessibility

  • ARIA Labels: Proper labels for screen readers
  • Reduced Motion: Respect user's motion preferences
  • Focus Management: Proper focus handling during loading states
  • Color Contrast: Sufficient contrast for visibility

Performance Tips

  1. CSS Animations: Use CSS animations when possible for better performance
  2. Reduced Motion: Respect prefers-reduced-motion media query
  3. Optimized Bundles: Tree-shake unused loader variants
  4. Lazy Loading: Load heavy animations only when needed

Troubleshooting

Check if Framer Motion is properly installed and the component is wrapped in a motion provider.

Consider using CSS-only animations for better performance on low-end devices.

Ensure proper ARIA labels and test with screen readers.

Check for conflicting CSS classes or Tailwind CSS purging issues.