Components

Loader

A fun loader component.

Loading
Loading

Code

tsx
198 lines
"use client"

import "./style/Loader.css"

import { cn } from "@/lib/utils"

type LoaderType = "dots" | "spinner"

type LoaderSize = "sm" | "md" | "lg" | number

type LoaderColor = "black" | "yellow" | "purple" | "green" | "pink" | "blue" | string

type LoaderMotion = "quick" | "normal" | "slow" | "slower"

type LoaderProps = {
    type?: LoaderType
    size?: LoaderSize
    color?: LoaderColor
    motion?: LoaderMotion
    speed?: number
    label?: string
    showLabel?: boolean
    className?: string
    labelClassName?: string
}

const colorMap: Record<string, string> = {
    black: "#111111",
    yellow: "#fde047",
    purple: "#c084fc",
    green: "#86efac",
    pink: "#f9a8d4",
    blue: "#93c5fd",
}

const sizeMap: Record<Exclude<LoaderSize, number>, number> = {
    sm: 28,
    md: 44,
    lg: 64,
}

const motionMap: Record<LoaderMotion, number> = {
    quick: 0.55,
    normal: 0.9,
    slow: 1.25,
    slower: 1.7,
}

function getSize(size: LoaderSize) {
    return typeof size === "number" ? size : sizeMap[size]
}

function getColor(color: LoaderColor) {
    return colorMap[color] ?? color
}

function getSpeed(motion: LoaderMotion, speed?: number) {
    return speed ?? motionMap[motion]
}

function DotsLoader({
    size,
    color,
    speed,
}: {
    size: number
    color: string
    speed: number
}) {
    const dot = Math.max(5, size * 0.18)
    const gap = dot * 0.9
    const width = dot * 3 + gap * 2
    const height = size

    return (
        <span
            className="inline-flex items-center justify-center"
            style={{
                width,
                height,
                ["--loader-speed" as string]: `${speed}s`,
            }}
            aria-hidden="true"
        >
            {[0, 1, 2].map((i) => (
                <span
                    key={i}
                    className="mx-[2px] inline-block rounded-full border-2 border-black sketch-loader-dot-bounce"
                    style={{
                        width: dot,
                        height: dot,
                        backgroundColor: color,
                        animationDelay: `${i * 0.12}s`,
                        boxShadow: "2px 2px 0 #111",
                    }}
                />
            ))}
        </span>
    )
}

function SpinnerLoader({
    size,
    color,
    speed,
}: {
    size: number
    color: string
    speed: number
}) {
    const dot = Math.max(6, size * 0.16)
    const orbit = Math.max(10, size * 0.34)

    return (
        <span
            className="relative inline-flex items-center justify-center sketch-loader-spinner"
            style={{
                width: size,
                height: size,
                ["--loader-speed" as string]: `${speed}s`,
                ["--loader-orbit" as string]: `${orbit}px`,
            }}
            aria-hidden="true"
        >
            {[0, 1, 2].map((i) => (
                <span
                    key={i}
                    className="absolute left-1/2 top-1/2 rounded-full border-2 border-black sketch-loader-orbit-dot"
                    style={{
                        width: dot,
                        height: dot,
                        backgroundColor: color,
                        boxShadow: "2px 2px 0 #111",
                        animationDelay: `${-(speed / 3) * i}s`,
                    }}
                />
            ))}
        </span>
    )
}

export function Loader({
    type = "dots",
    size = "md",
    color = "purple",
    motion = "normal",
    speed,
    label = "Loading",
    showLabel = false,
    className,
    labelClassName,
}: LoaderProps) {
    const finalSize = getSize(size)
    const finalColor = getColor(color)
    const finalSpeed = getSpeed(motion, speed)

    return (
        <div
            role="status"
            aria-label={label}
            className={cn(
                "inline-flex items-center justify-center gap-3 text-black",
                className
            )}
        >
            {type === "dots" && (
                <DotsLoader
                    size={finalSize}
                    color={finalColor}
                    speed={finalSpeed}
                />
            )}

            {type === "spinner" && (
                <SpinnerLoader
                    size={finalSize}
                    color={finalColor}
                    speed={finalSpeed}
                />
            )}

            {showLabel && (
                <span
                    className={cn(
                        "text-sm font-black tracking-wide",
                        "after:inline-block after:content-['...']",
                        labelClassName
                    )}
                >
                    {label}
                </span>
            )}

            <span className="sr-only">{label}</span>
        </div>
    )
}

CSS

loader.css:

css
48 lines
.sketch-loader-dot-bounce {
    animation: sketch-loader-bounce var(--loader-speed) ease-in-out infinite;
}

.sketch-loader-spinner {
    animation: sketch-loader-spin var(--loader-speed) linear infinite;
}

.sketch-loader-orbit-dot {
    animation: sketch-loader-orbit var(--loader-speed) linear infinite;
}

@keyframes sketch-loader-bounce {
    0%,
    80%,
    100% {
        transform: translateY(0) rotate(0deg);
    }

    40% {
        transform: translateY(-45%) rotate(-8deg);
    }
}

@keyframes sketch-loader-spin {
    to {
        transform: rotate(360deg);
    }
}

@keyframes sketch-loader-orbit {
    from {
        transform: translate(-50%, -50%) rotate(0deg)
            translateX(var(--loader-orbit)) rotate(0deg);
        opacity: 1;
    }

    50% {
        opacity: 0.65;
    }

    to {
        transform: translate(-50%, -50%) rotate(360deg)
            translateX(var(--loader-orbit)) rotate(-360deg);
        opacity: 1;
    }
}

Usage

tsx
<Loader
    type="dots"
    color="purple"
    size="md"
    motion="normal"
/>

Props

PropTypeDefaultDescription
typeLoaderType"dots"Controls the loader style.
sizeLoaderSize"md"Controls the overall loader size.
colorLoaderColor"purple"Controls the loader color theme.
motionLoaderMotion"normal"Controls the animation speed preset.
speednumber-Custom animation speed override.
labelstring"Loading"Text displayed below or beside the loader.
showLabelbooleanfalseDisplays the loading label.
classNamestring-Additional classes applied to the loader wrapper.
labelClassNamestring-Additional classes applied to the label.

Types

  • dots
  • spinner

Sizes

  • sm
  • md
  • lg
  • number

Colors

  • black
  • purple
  • yellow
  • green
  • pink
  • blue
  • string

Motion Presets

  • slower
  • slow
  • normal
  • quick