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'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>
</>
);
}