Aura Design System

Loading Indicator

A loading status that morphs between shapes.

Preview

Loading content
import { LoadingIndicator } from "@/components/ui/LoadingIndicator";

export function LoadingIndicatorDemo() {
  return <LoadingIndicator label="Loading content" />;
}

Installation

Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces

pnpm dlx shadcn@latest add @aura/loading-indicator

Manual

Copy and paste the class names utility into your utils/class-names.ts file.

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Copy and paste the shape component into your components/ui/Shape.tsx file.

"use client";

import * as React from "react";
import { cn } from "@/utils/class-names";

const shapePaths = {
  circle: "circle(50% at 50% 50%)",
  square: "inset(0% round 12%)",
  diamond: "polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)",
  arch: "polygon(0% 100%, 0% 42%, 18% 14%, 50% 0%, 82% 14%, 100% 42%, 100% 100%)",
  pill: "inset(18% 0% round 999px)",
} as const;

type ShapeName = keyof typeof shapePaths;

function Shape({
  className,
  name = "circle",
  size = "default",
  ...props
}: React.ComponentProps<"div"> & {
  name?: ShapeName;
  size?: "sm" | "default" | "lg";
}) {
  return (
    <div
      data-slot="shape"
      data-shape={name}
      data-size={size}
      style={{ clipPath: shapePaths[name] }}
      className={cn(
        "bg-accent-9 transition duration-300 ease-in-out motion-reduce:transition-none",
        size === "sm" && "size-3",
        size === "default" && "size-8",
        size === "lg" && "size-12",
        className,
      )}
      {...props}
    />
  );
}

function ShapeMorph({
  names = ["circle", "diamond", "arch", "pill", "square"],
  interval = 900,
}: {
  names?: ShapeName[];
  interval?: number;
}) {
  const [index, setIndex] = React.useState(0);
  React.useEffect(() => {
    const query = window.matchMedia("(prefers-reduced-motion: reduce)");
    if (query.matches) return;
    const timer = window.setInterval(() => {
      setIndex((current) => (current + 1) % names.length);
    }, interval);
    return () => window.clearInterval(timer);
  }, [interval, names.length]);

  return <Shape name={names[index]} size="sm" />;
}

export { Shape, ShapeMorph, shapePaths };
export type { ShapeName };

Copy and paste the LoadingIndicator component into your components/ui/LoadingIndicator.tsx file.

"use client";

import * as React from "react";
import { cn } from "@/utils/class-names";
import { ShapeMorph } from "@/components/ui/Shape";

function LoadingIndicator({
  className,
  label = "Loading",
  ...props
}: React.ComponentProps<"div"> & { label?: string }) {
  return (
    <div
      role="status"
      data-slot="loading-indicator"
      className={cn("inline-flex items-center gap-0.5", className)}
      {...props}
    >
      <ShapeMorph />
      <span className="sr-only">{label}</span>
    </div>
  );
}

export { LoadingIndicator };