A solid glowing plane with flowing vertex displacement that mimics an aurora borealis.
three @react-three/fiber @react-three/drei<AuroraRibbon /> inside your own <Canvas>.import { useMemo, useRef } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
import type { Mesh, BufferAttribute } from 'three';
const SEGX = 96;
const SEGY = 48;
export default function AuroraRibbon({ scale = 1 }: { color?: string; scale?: number }) {
const ref = useRef<Mesh>(null);
// Fixed cyan -> violet -> magenta gradient baked as per-vertex colors across X.
const colors = useMemo(() => {
const nx = SEGX + 1;
const ny = SEGY + 1;
const c1 = new THREE.Color('#22e0ff');
const c2 = new THREE.Color('#8a5cff');
const c3 = new THREE.Color('#ff2fd0');
const tmp = new THREE.Color();
const arr = new Float32Array(nx * ny * 3);
for (let iy = 0; iy < ny; iy++) {
for (let ix = 0; ix < nx; ix++) {
const t = ix / (nx - 1);
if (t < 0.5) tmp.copy(c1).lerp(c2, t * 2);
else tmp.copy(c2).lerp(c3, (t - 0.5) * 2);
const idx = (iy * nx + ix) * 3;
arr[idx] = tmp.r;
arr[idx + 1] = tmp.g;
arr[idx + 2] = tmp.b;
}
}
return arr;
}, []);
useFrame((state) => {
const mesh = ref.current;
if (!mesh) return;
const t = state.clock.elapsedTime;
const pos = mesh.geometry.attributes.position as BufferAttribute;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const y = pos.getY(i);
pos.setZ(i, Math.sin(x * 1.4 + t) * 0.4 + Math.cos(y * 1.3 + t * 0.7) * 0.28);
}
pos.needsUpdate = true;
});
return (
<mesh ref={ref} rotation={[-0.5, 0, 0]} scale={scale}>
<planeGeometry args={[5, 3, SEGX, SEGY]}>
<bufferAttribute attach="attributes-color" args={[colors, 3]} />
</planeGeometry>
<meshBasicMaterial vertexColors toneMapped={false} side={THREE.DoubleSide} transparent opacity={0.95} />
</mesh>
);
}