Kanso UI LogoKanso UI
Components/antigravity
effects

Antigravity Particles

A gorgeous React Three Fiber canvas component rendering magnetic floating particles that orbit the mouse cursor on hover.

particlesmagnetorbitwaveinteractivefiberwebgl

Preview

React Three Fiber Instanced Field
Adjust Particle Instancing Props
Particle Geometry
Particles Count250
Magnet Radius10
Orbit Radius8
Particle Scale2
Enable Multi-Color Particles
Particle Color (Single color disabled)

🖲Usage

example-usage.tsx
import { Antigravity } from "@/components/kanso/antigravity"

export default function AntigravityDemo() {
  return (
    <div className="w-full h-[350px] border border-zinc-200 dark:border-zinc-800 bg-zinc-950 rounded-xl overflow-hidden">
      <Antigravity
        count={250}
        magnetRadius={8}
        ringRadius={8}
        color="#c084fc"
        particleShape="capsule"
      />
    </div>
  )
}

Installation

1

Create folder & copy source

Create a folder named kanso inside your project's components directory (i.e. components/kanso/). Copy the source code shown in the next section, and paste it into a file named antigravity.tsx inside it.

2

Install dependencies

pnpm add three @react-three/fiber
3

Required helper files

Ensure your project has the following helper files configured:

  • lib/utils

<>Source Code

antigravity.tsx
'use client';

import * as React from 'react';
import * as THREE from 'three';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { cn } from '@/lib/utils';

export interface AntigravityProps {
  count?: number;
  magnetRadius?: number;
  ringRadius?: number;
  waveSpeed?: number;
  waveAmplitude?: number;
  particleSize?: number;
  lerpSpeed?: number;
  color?: string;
  colors?: string[];
  autoAnimate?: boolean;
  particleVariance?: number;
  rotationSpeed?: number;
  depthFactor?: number;
  pulseSpeed?: number;
  particleShape?: 'capsule' | 'sphere' | 'box' | 'tetrahedron';
  fieldStrength?: number;
}

function createRandom(seed: number) {
  let s = seed;
  return () => {
    const x = Math.sin(s++) * 10000;
    return x - Math.floor(x);
  };
}

const AntigravityInner: React.FC<AntigravityProps> = ({
  count = 300,
  magnetRadius = 10,
  ringRadius = 10,
  waveSpeed = 0.4,
  waveAmplitude = 1,
  particleSize = 2,
  lerpSpeed = 0.1,
  color = '#FF9FFC',
  colors,
  autoAnimate = false,
  particleVariance = 1,
  rotationSpeed = 0,
  depthFactor = 1,
  pulseSpeed = 3,
  particleShape = 'capsule',
  fieldStrength = 10,
}) => {
  const meshRef = React.useRef<THREE.InstancedMesh>(null);
  const { viewport } = useThree();
  const dummy = React.useMemo(() => new THREE.Object3D(), []);
  const hasColors = colors && colors.length > 0;

  const lastMousePos = React.useRef({ x: 0, y: 0 });
  const lastMouseMoveTime = React.useRef(0);
  const virtualMouse = React.useRef({ x: 0, y: 0 });

  const particles = React.useMemo(() => {
    const temp = [];
    const width = viewport.width || 100;
    const height = viewport.height || 100;
    const random = createRandom(42);

    for (let i = 0; i < count; i++) {
      const t = random() * 100;
      const factor = 20 + random() * 100;
      const speed = 0.01 + random() / 200;
      const xFactor = -50 + random() * 100;
      const yFactor = -50 + random() * 100;
      const zFactor = -50 + random() * 100;

      const x = (random() - 0.5) * width;
      const y = (random() - 0.5) * height;
      const z = (random() - 0.5) * 20;

      const randomRadiusOffset = (random() - 0.5) * 2;

      temp.push({
        t,
        factor,
        speed,
        xFactor,
        yFactor,
        zFactor,
        mx: x,
        my: y,
        mz: z,
        cx: x,
        cy: y,
        cz: z,
        vx: 0,
        vy: 0,
        vz: 0,
        randomRadiusOffset,
      });
    }
    return temp;
  }, [count, viewport.width, viewport.height]);

  useFrame((state) => {
    const mesh = meshRef.current;
    if (!mesh) return;

    const { viewport: v, pointer: m } = state;

    const mouseDist = Math.sqrt(
      Math.pow(m.x - lastMousePos.current.x, 2) +
        Math.pow(m.y - lastMousePos.current.y, 2)
    );

    if (mouseDist > 0.001) {
      lastMouseMoveTime.current = Date.now();
      lastMousePos.current = { x: m.x, y: m.y };
    }

    let destX = (m.x * v.width) / 2;
    let destY = (m.y * v.height) / 2;

    if (autoAnimate && Date.now() - lastMouseMoveTime.current > 2000) {
      const time = state.clock.getElapsedTime();
      destX = Math.sin(time * 0.5) * (v.width / 4);
      destY = Math.cos(time * 0.5 * 2) * (v.height / 4);
    }

    const smoothFactor = 0.05;
    virtualMouse.current.x += (destX - virtualMouse.current.x) * smoothFactor;
    virtualMouse.current.y += (destY - virtualMouse.current.y) * smoothFactor;

    const targetX = virtualMouse.current.x;
    const targetY = virtualMouse.current.y;

    const globalRotation = state.clock.getElapsedTime() * rotationSpeed;
    const colorObj = new THREE.Color();

    particles.forEach((particle, i) => {
      let { t } = particle;
      const { speed, mx, my, mz, cz, randomRadiusOffset } = particle;

      t = particle.t += speed / 2;

      const projectionFactor = 1 - cz / 50;
      const projectedTargetX = targetX * projectionFactor;
      const projectedTargetY = targetY * projectionFactor;

      const dx = mx - projectedTargetX;
      const dy = my - projectedTargetY;
      const dist = Math.sqrt(dx * dx + dy * dy);

      const targetPos = { x: mx, y: my, z: mz * depthFactor };

      if (dist < magnetRadius) {
        const angle = Math.atan2(dy, dx) + globalRotation;

        const wave = Math.sin(t * waveSpeed + angle) * (0.5 * waveAmplitude);
        const deviation = randomRadiusOffset * (5 / (fieldStrength + 0.1));

        const currentRingRadius = ringRadius + wave + deviation;

        targetPos.x = projectedTargetX + currentRingRadius * Math.cos(angle);
        targetPos.y = projectedTargetY + currentRingRadius * Math.sin(angle);
        targetPos.z =
          mz * depthFactor + Math.sin(t) * (1 * waveAmplitude * depthFactor);
      }

      particle.cx += (targetPos.x - particle.cx) * lerpSpeed;
      particle.cy += (targetPos.y - particle.cy) * lerpSpeed;
      particle.cz += (targetPos.z - particle.cz) * lerpSpeed;

      dummy.position.set(particle.cx, particle.cy, particle.cz);

      dummy.lookAt(projectedTargetX, projectedTargetY, particle.cz);
      dummy.rotateX(Math.PI / 2);

      const currentDistToMouse = Math.sqrt(
        Math.pow(particle.cx - projectedTargetX, 2) +
          Math.pow(particle.cy - projectedTargetY, 2)
      );

      const distFromRing = Math.abs(currentDistToMouse - ringRadius);
      let scaleFactor = 1 - distFromRing / 10;

      scaleFactor = Math.max(0, Math.min(1, scaleFactor));

      const finalScale =
        scaleFactor *
        (0.8 + Math.sin(t * pulseSpeed) * 0.2 * particleVariance) *
        particleSize;
      dummy.scale.set(finalScale, finalScale, finalScale);

      dummy.updateMatrix();

      mesh.setMatrixAt(i, dummy.matrix);

      // Set individual instance color
      if (hasColors) {
        colorObj.set(colors[i % colors.length]);
        mesh.setColorAt(i, colorObj);
      } else {
        colorObj.set(color);
        mesh.setColorAt(i, colorObj);
      }
    });

    mesh.instanceMatrix.needsUpdate = true;
    if (mesh.instanceColor) {
      mesh.instanceColor.needsUpdate = true;
    }
  });

  return (
    <instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
      {particleShape === 'capsule' && (
        <capsuleGeometry args={[0.1, 0.4, 4, 8]} />
      )}
      {particleShape === 'sphere' && <sphereGeometry args={[0.2, 16, 16]} />}
      {particleShape === 'box' && <boxGeometry args={[0.3, 0.3, 0.3]} />}
      {particleShape === 'tetrahedron' && <tetrahedronGeometry args={[0.3]} />}
      <meshBasicMaterial color={hasColors ? '#ffffff' : color} />
    </instancedMesh>
  );
};

export interface AntigravityWrapperProps
  extends React.HTMLAttributes<HTMLDivElement>, AntigravityProps {}

const Antigravity = React.forwardRef<HTMLDivElement, AntigravityWrapperProps>(
  (
    {
      count,
      magnetRadius,
      ringRadius,
      waveSpeed,
      waveAmplitude,
      particleSize,
      lerpSpeed,
      color,
      colors,
      autoAnimate,
      particleVariance,
      rotationSpeed,
      depthFactor,
      pulseSpeed,
      particleShape,
      fieldStrength,
      className,
      style,
      ...props
    },
    ref
  ) => {
    return (
      <div
        ref={ref}
        className={cn('w-full h-full relative overflow-hidden', className)}
        style={style}
        {...props}
      >
        <Canvas camera={{ position: [0, 0, 50], fov: 35 }}>
          <AntigravityInner
            count={count}
            magnetRadius={magnetRadius}
            ringRadius={ringRadius}
            waveSpeed={waveSpeed}
            waveAmplitude={waveAmplitude}
            particleSize={particleSize}
            lerpSpeed={lerpSpeed}
            color={color}
            colors={colors}
            autoAnimate={autoAnimate}
            particleVariance={particleVariance}
            rotationSpeed={rotationSpeed}
            depthFactor={depthFactor}
            pulseSpeed={pulseSpeed}
            particleShape={particleShape}
            fieldStrength={fieldStrength}
          />
        </Canvas>
      </div>
    );
  }
);

Antigravity.displayName = 'Antigravity';

export { Antigravity };

Props

Command Palette

Search for a command to run...