Loaders

Ceiling Fan Loader

A loading spinner styled like a spinning ceiling fan, with three brushed-metal blades rotating around a metallic hub.

Loading preview
Advertisement
Ad space reserved for a future placement

Source code

"use client";

import { useId } from "react";

type CeilingFanLoaderSize = "sm" | "md" | "lg";

interface CeilingFanLoaderProps {
  size?: CeilingFanLoaderSize;
  label?: string;
  className?: string;
}

const SIZE_PX: Record<CeilingFanLoaderSize, number> = {
  sm: 32,
  md: 48,
  lg: 64,
};

export function CeilingFanLoader({ size = "md", label = "Loading", className }: CeilingFanLoaderProps) {
  const px = SIZE_PX[size];
  const gradientId = useId();

  return (
    <div
      role="status"
      aria-label={label}
      className={["ceiling-fan-loader", className].filter(Boolean).join(" ")}
      style={{ width: px, height: px }}
    >
      <svg viewBox="0 0 100 100" fill="none">
        <defs>
          <linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="100%">
            <stop offset="0%" stopColor="#f1f5f9" />
            <stop offset="25%" stopColor="#94a3b8" />
            <stop offset="50%" stopColor="#e2e8f0" />
            <stop offset="75%" stopColor="#64748b" />
            <stop offset="100%" stopColor="#cbd5e1" />
          </linearGradient>
        </defs>
        <g className="ceiling-fan-loader-blades">
          <ellipse cx="50" cy="28" rx="7" ry="20" fill={`url(#${gradientId})`} transform="rotate(0 50 50)" />
          <ellipse cx="50" cy="28" rx="7" ry="20" fill={`url(#${gradientId})`} transform="rotate(120 50 50)" />
          <ellipse cx="50" cy="28" rx="7" ry="20" fill={`url(#${gradientId})`} transform="rotate(240 50 50)" />
          <circle cx="50" cy="50" r="8" fill={`url(#${gradientId})`} />
        </g>
      </svg>
      <style jsx>{`
        .ceiling-fan-loader {
          display: inline-block;
        }
        .ceiling-fan-loader svg {
          width: 100%;
          height: 100%;
        }
        .ceiling-fan-loader-blades {
          transform-origin: 50px 50px;
          animation: ceiling-fan-spin 1.1s linear infinite;
        }
        @keyframes ceiling-fan-spin {
          to {
            transform: rotate(360deg);
          }
        }
        @media (prefers-reduced-motion: reduce) {
          .ceiling-fan-loader-blades {
            animation-duration: 3s;
          }
        }
      `}</style>
    </div>
  );
}

Usage

Import the component and use it like this.

import { CeilingFanLoader } from "./CeilingFanLoader";

export default function Example() {
  return <CeilingFanLoader size="md" label="Loading" />;
}

Dependencies

No external dependencies.

Usage notes

Pass size (sm/md/lg) and an optional label for the accessible status announcement.