Skip to content
All Taste components

Loaders

Spinners

Four small loading indicators — ring, dots, bars and pulse — in your brand colour.

ring · dots · bars · pulse

Ring
Dots
Bars
Pulse
/* globals.css
@keyframes dot { 0%, 80%, 100% { transform: scale(.6); opacity: .4; } 40% { transform: scale(1); opacity: 1; } }
@keyframes bar { 0%, 100% { transform: scaleY(.4); } 50% { transform: scaleY(1); } }
*/
import { cn } from "@/lib/utils";

export function RingSpinner({ className }: { className?: string }) {
  return (
    <span role="status" aria-label="Loading" className={cn("inline-block size-6 animate-spin rounded-full border-[2.5px] border-primary-soft border-t-primary motion-reduce:animate-none", className)} />
  );
}

export function DotsSpinner() {
  return (
    <span role="status" aria-label="Loading" className="inline-flex gap-1.5">
      {[0, 1, 2].map((i) => (
        <i key={i} className="size-[7px] rounded-full bg-primary animate-[dot_1.2s_cubic-bezier(.2,.7,.1,1)_infinite] motion-reduce:animate-none" style={{ animationDelay: i * 160 + "ms" }} />
      ))}
    </span>
  );
}

export function BarsSpinner() {
  return (
    <span role="status" aria-label="Loading" className="inline-flex h-[22px] items-center gap-[3px]">
      {[0, 1, 2, 3, 4].map((i) => (
        <i key={i} className="h-full w-1 rounded-sm bg-primary animate-[bar_1s_ease-in-out_infinite] motion-reduce:animate-none" style={{ animationDelay: i * 100 + "ms" }} />
      ))}
    </span>
  );
}

export function PulseDot({ label = "Live" }: { label?: string }) {
  return (
    <span role="status" aria-label={label} className="relative inline-flex size-3">
      <span aria-hidden className="absolute inset-0 animate-ping rounded-full bg-primary opacity-50 motion-reduce:animate-none" />
      <span className="relative size-3 rounded-full bg-primary" />
    </span>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.