Skip to content
Plinth

Original by Plinth · MIT

Backgrounds

Fluid Orb

Three coloured gradients screened into a glass sphere that drifts on its own and leans toward your cursor.

#background#orb#gradient#blend-mode#pointer

Install

npx shadcn@latest add https://plinthui.com/r/fluid-orb.json
Open in v0
'use client';

import { useRef } from 'react';

// The orb needs a dark ground: its three colour layers are composited with
// mix-blend-mode: screen, so on a light surface they wash out to nothing.
// Blend modes, keyframes and the pointer-driven custom properties are all
// outside what Tailwind can express, so the material lives in the style
// element below.
const css = `
.orb {
  position: relative;
  width: min(58%, 190px);
  aspect-ratio: 1;
  border-radius: 50%;
  /* The drift is a spring the pointer pulls on; --ox/--oy are set in JS. */
  transform: translate3d(var(--ox, 0px), var(--oy, 0px), 0);
  transition: transform 1.1s cubic-bezier(0.16, 1, 0.3, 1);
  filter: blur(0.5px);
  background: radial-gradient(circle at 50% 50%, #1b0f2f 0%, #0a0614 72%);
  overflow: hidden;
}

/* Three offset gradients screened together. One gradient reads as a sticker;
   three drifting at different periods never quite repeat. */
.orb__layer {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  mix-blend-mode: screen;
  will-change: transform;
}
.orb__layer--a {
  background: radial-gradient(circle at 26% 24%, #ffc27a 0%, #e0612a 25%, rgba(224, 97, 42, 0) 55%);
  animation: orb-swirl-a 9s ease-in-out infinite;
}
.orb__layer--b {
  background: radial-gradient(circle at 76% 66%, #5fd8ff 0%, #2455c8 27%, rgba(36, 85, 200, 0) 57%);
  animation: orb-swirl-b 11s ease-in-out infinite;
}
.orb__layer--c {
  background: radial-gradient(circle at 48% 82%, #ff8ae8 0%, #7d24c9 29%, rgba(125, 36, 201, 0) 59%);
  animation: orb-swirl-c 13s ease-in-out infinite;
}

/* A bloom, so the orb sits in the dark rather than on top of it. */
.orb::after {
  content: "";
  position: absolute;
  inset: -34%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(150, 120, 255, 0.28) 0%, rgba(150, 120, 255, 0) 62%);
  pointer-events: none;
}

/* The meniscus that makes it read as a sphere rather than a blur. */
.orb__shell {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 32% 24%, rgba(255, 255, 255, 0.34) 0%, rgba(255, 255, 255, 0.04) 17%, transparent 46%);
  box-shadow:
    inset 0 0 30px rgba(255, 255, 255, 0.14),
    inset 0 -14px 34px rgba(4, 2, 18, 0.55);
}

@keyframes orb-swirl-a {
  0%, 100% { transform: translate(-7%, -5%) scale(1.05); }
  50%      { transform: translate(8%, 6%) scale(0.92); }
}
@keyframes orb-swirl-b {
  0%, 100% { transform: translate(6%, 4%) scale(0.95); }
  50%      { transform: translate(-8%, -7%) scale(1.08); }
}
@keyframes orb-swirl-c {
  0%, 100% { transform: translate(2%, -8%) scale(1); }
  50%      { transform: translate(-4%, 8%) scale(1.06); }
}

@media (prefers-reduced-motion: reduce) {
  .orb__layer { animation: none; }
  .orb { transition: none; }
}
`;

/** How far the orb may lean toward the pointer, in pixels. */
const REACH = 26;

export function FluidOrb() {
  const orbRef = useRef<HTMLDivElement>(null);

  function lean(event: React.PointerEvent<HTMLDivElement>) {
    const orb = orbRef.current;
    if (!orb) return;
    const r = event.currentTarget.getBoundingClientRect();
    const dx = (event.clientX - (r.left + r.width / 2)) / (r.width / 2);
    const dy = (event.clientY - (r.top + r.height / 2)) / (r.height / 2);
    // Clamped so a pointer far outside the stage does not fling it.
    orb.style.setProperty('--ox', `${Math.max(-1, Math.min(1, dx)) * REACH}px`);
    orb.style.setProperty('--oy', `${Math.max(-1, Math.min(1, dy)) * REACH}px`);
  }

  function settle() {
    const orb = orbRef.current;
    if (!orb) return;
    orb.style.setProperty('--ox', '0px');
    orb.style.setProperty('--oy', '0px');
  }

  return (
    <>
      <style>{css}</style>
      <div
        onPointerMove={lean}
        onPointerLeave={settle}
        style={{
          position: 'relative',
          display: 'grid',
          placeItems: 'center',
          minHeight: 320,
          background: '#07070b',
          cursor: 'crosshair',
        }}
      >
        <div className="orb" ref={orbRef}>
          <span className="orb__layer orb__layer--a" />
          <span className="orb__layer orb__layer--b" />
          <span className="orb__layer orb__layer--c" />
          <span className="orb__shell" />
        </div>
      </div>
    </>
  );
}

More in Backgrounds

Similar tags