bottega

primitives

Three Body Loader

A compact loading spinner: three dots orbit a shared center while each wobbles in and out on its own phase, reading as an organic three-body motion rather than a rigid spin. Pure CSS, no JS (server component). size and speed are props (mapped to --uib-size / --uib-speed); the dot color comes from --uib-color, defaulting to the --primary token. Announced as role=status with an overridable label. Ported from a uiverse.io loader by dovatgabriel.

Ported from dovatgabriel (MIT)


Install

1. Register the namespace (once per project):

json
// components.json — register the @bottega namespace once
{
  "registries": {
    "@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
  }
}

2. Add the component:

bash
npx shadcn add @bottega/three-body-loader

Usage

Usagetsx
<ThreeBodyLoader />

Props

PropTypeDefaultDescription
sizenumber35Overall size in px (maps to --uib-size). Default 35.
speednumber0.8Animation period in seconds — lower is faster (maps to --uib-speed). Default 0.8.
colorstringDot color. Any CSS color; defaults to the --primary token.
labelstringLoadingAccessible label announced by screen readers. Default "Loading".

Source

three-body-loader.tsxtsx
import type React from "react";
import styles from "./three-body-loader.module.css";

export interface ThreeBodyLoaderProps
  extends React.HTMLAttributes<HTMLDivElement> {
  /** Overall size in px (maps to --uib-size). Default 35. */
  size?: number;
  /** Animation period in seconds — lower is faster (maps to --uib-speed). Default 0.8. */
  speed?: number;
  /** Dot color. Any CSS color; defaults to the --primary token. */
  color?: string;
  /** Accessible label announced by screen readers. Default "Loading". */
  label?: string;
}

/**
 * Three Body Loader — three dots orbit a shared center while each wobbles in and
 * out on its own phase, reading as an organic three-body motion rather than a
 * rigid spin. Pure CSS, no JS (server component).
 *
 * `size` and `speed` are exposed as props (mapped to --uib-size / --uib-speed);
 * the dot color comes from --uib-color, defaulting to the --primary token.
 * Announced as role="status" with an overridable label. All motion is removed
 * under prefers-reduced-motion (static dots — see the module CSS).
 *
 * Ported from a uiverse.io loader by dovatgabriel.
 */
export function ThreeBodyLoader({
  size = 35,
  speed = 0.8,
  color,
  label = "Loading",
  className = "",
  style,
  ...props
}: ThreeBodyLoaderProps) {
  return (
    <div
      role="status"
      aria-label={label}
      className={[styles.root, className].filter(Boolean).join(" ")}
      style={
        {
          "--uib-size": `${size}px`,
          "--uib-speed": `${speed}s`,
          ...(color ? { "--uib-color": color } : {}),
          ...style,
        } as React.CSSProperties
      }
      {...props}
    >
      <div className={styles.dot} />
      <div className={styles.dot} />
      <div className={styles.dot} />
    </div>
  );
}

Dependencies

  • @bottega/tokens