CoreAnimated Backgrounds and Visual Effectsno dependencies

OrbitingParticles

Canvas orbital particle field with elliptical trajectories, Z-depth scale and opacity modulation, pointer gravity shift, and automatic RAF cleanup.

Preview

Orbital Depth Particle Field

Customize

4 props
34
0.65
0.55

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add orbiting-particles

Writes components/ui/orbiting-particles.tsx and components/styles/orbiting-particles.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { OrbitingParticles } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <OrbitingParticles />6  );7}

Props

PropTypeDefault
particleCount

Number of orbiting particles.

number34
orbitSpeed

Angular velocity multiplier.

number0.65
eccentricity

Vertical-to-horizontal ellipse aspect ratio.

number0.55
particleColor

Hex color of the orbiting particles.

string'#e4e4e7'
label

Center caption.

string'Orbital Depth Particle Field'

Accessibility

Marks the canvas aria-hidden='true', cleans up requestAnimationFrame and resize listeners on unmount, and renders a static orbital ring under prefers-reduced-motion.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/orbiting-particles.tsx
1"use client";2 3import * as React from "react";4import styles from "../styles/orbiting-particles.module.css";5 6export interface OrbitingParticlesProps {7  /** Number of orbiting particles */8  particleCount?: number;9  /** Angular speed multiplier */10  orbitSpeed?: number;11  /** Ellipse eccentricity ratio */12  eccentricity?: number;13  /** Particle color */14  particleColor?: string;15  /** Optional center caption */16  label?: string;17  /** Optional className */18  className?: string;19}20 21export function OrbitingParticles({22  particleCount = 34,23  orbitSpeed = 0.65,24  eccentricity = 0.55,25  particleColor = "#e4e4e7",26  label = "Orbital Depth Particle Field",27  className = "",28}: OrbitingParticlesProps) {29  const canvasRef = React.useRef<HTMLCanvasElement>(null);30  const offsetRef = React.useRef({ x: 0, y: 0 });31 32  React.useEffect(() => {33    const canvas = canvasRef.current;34    if (!canvas) return;35    const ctx = canvas.getContext("2d");36    if (!ctx) return;37 38    const prefersReduced =39      typeof window !== "undefined" &&40      window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;41 42    let rafId = 0;43    const count = Math.min(Math.max(particleCount, 10), 80);44 45    const resize = () => {46      const parent = canvas.parentElement;47      if (!parent) return;48      canvas.width = parent.clientWidth;49      canvas.height = parent.clientHeight;50    };51    resize();52 53    const particles = Array.from({ length: count }, (_, i) => ({54      angle: (i / count) * Math.PI * 2,55      radius: 36 + ((i * 19) % 110),56      speed: (0.004 + (i % 5) * 0.0018) * orbitSpeed,57      size: 1.1 + (i % 3) * 0.65,58    }));59 60    const render = () => {61      ctx.clearRect(0, 0, canvas.width, canvas.height);62      const cx = canvas.width / 2 + offsetRef.current.x;63      const cy = canvas.height / 2 + offsetRef.current.y;64 65      for (const p of particles) {66        if (!prefersReduced) {67          p.angle += p.speed;68        }69        const x = cx + Math.cos(p.angle) * p.radius;70        const y = cy + Math.sin(p.angle) * p.radius * eccentricity;71        const depth = (Math.sin(p.angle) + 1) * 0.5; // 0 (back) to 1 (front)72 73        ctx.beginPath();74        ctx.arc(x, y, p.size * (0.65 + depth * 0.65), 0, Math.PI * 2);75        ctx.fillStyle = particleColor;76        ctx.globalAlpha = 0.2 + depth * 0.65;77        ctx.fill();78      }79 80      if (!prefersReduced) {81        rafId = window.requestAnimationFrame(render);82      }83    };84 85    render();86    window.addEventListener("resize", resize);87    return () => {88      window.removeEventListener("resize", resize);89      window.cancelAnimationFrame(rafId);90    };91  }, [particleCount, orbitSpeed, eccentricity, particleColor]);92 93  return (94    <div95      onPointerMove={(e) => {96        const rect = e.currentTarget.getBoundingClientRect();97        offsetRef.current = {98          x: ((e.clientX - rect.left) / rect.width - 0.5) * 28,99          y: ((e.clientY - rect.top) / rect.height - 0.5) * 18,100        };101      }}102      onPointerLeave={() => {103        offsetRef.current = { x: 0, y: 0 };104      }}105      className={`${styles.stage} ${className}`.trim()}106    >107      <canvas ref={canvasRef} aria-hidden="true" className={styles.canvas} />108      <span className={styles.caption}>{label}</span>109    </div>110  );111}