HelixLoader
PreviewCode
// preview
Color from
#8e8e97
Color mid
#dbc2ff
Color to
#ffffff
// install
pnpmnpmyarnbun
npx shadcn@latest add "https://designpass.dev/r/HelixLoader-TS-TW.json"Install the HelixLoader component from DesignPass into this project by running:
npx shadcn@latest add "https://designpass.dev/r/HelixLoader-TS-TW.json"
If the project has no components.json yet, run `npx shadcn@latest init` first.
Then show me a minimal usage example.// source
tsjs
twcss
/*!
* HelixLoader, a DesignPass.dev component by Ernest Liu (ernestliu.com)
* Docs & live playground: https://designpass.dev/components/helix-loader
* MIT licensed, keep this notice in copies and adaptations.
*/
"use client";
import React, { useEffect, useRef, type CSSProperties } from "react";
export interface HelixLoaderProps {
/** Overall width of the weave (px); also sets how far the dots swing. */
width?: number;
/** Overall height of the weave (px); also sets the dot diameter. */
height?: number;
/** Seconds for one full orbit of a dot. */
speed?: number;
/** Gradient tone the dots take at the front of the orbit (dim base). */
colorFrom?: string;
/** Gradient accent, passed through mid-orbit. */
colorMid?: string;
/** Gradient tone at the back of the orbit (bright peak). */
colorTo?: string;
className?: string;
style?: CSSProperties;
}
const ROWS = 6;
/**
* Two ribbons of dots weaving through each other like a strand of DNA. Each dot
* swings left to right on a cosine while its depth (scale) tracks a sine, and
* instead of dimming with opacity at the front, it is recolored through a
* three-stop gradient (bright at the back, accent mid-swing, dim base up front).
* Every dot is one Web Animations API loop with a phase offset, so the weave
* emerges from twelve staggered copies of the same motion. Zero dependencies,
* honors prefers-reduced-motion.
*/
export default function HelixLoader({
width = 84,
height = 57,
speed = 1.2,
colorFrom = "#8e8e97",
colorMid = "#dbc2ff",
colorTo = "#ffffff",
className = "",
style,
}: HelixLoaderProps) {
const dots = useRef<(HTMLSpanElement | null)[]>([]);
useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const amplitude = width * 0.25;
const steps = 24;
// Sample the swing analytically: cosine drives the horizontal travel,
// sine drives the depth (scale), and the gradient color is keyed to the
// same depth at five checkpoints so it reads bright-at-back, dim-at-front.
const colors = [colorMid, colorFrom, colorMid, colorTo, colorMid];
const frames: Keyframe[] = [];
for (let i = 0; i <= steps; i += 1) {
const t = i / steps;
const angle = 2 * Math.PI * t;
const x = amplitude * Math.cos(angle);
const scale = 0.737 - 0.263 * Math.sin(angle);
// z-index tracks scale so the nearer (larger) dot always crosses in
// front of the smaller one where the two strands overlap.
const frame: Keyframe = {
offset: t,
transform: `translateX(${x}px) scale(${scale})`,
zIndex: Math.round(scale * 1000),
};
if (i % 6 === 0) frame.backgroundColor = colors[i / 6];
frames.push(frame);
}
// Half-orbit phase between the two strands, sixth-orbit between rows.
const strandDelay = -speed / 2;
const rowDelay = -speed / 6;
const animations = dots.current.map((dot, index) => {
const row = Math.floor(index / 2);
const strand = index % 2;
return dot?.animate(frames, {
duration: speed * 1000,
iterations: Infinity,
easing: "linear",
delay: (strand * strandDelay + row * rowDelay) * 1000,
});
});
return () => animations.forEach((animation) => animation?.cancel());
}, [width, speed, colorFrom, colorMid, colorTo]);
const dot = height / ROWS;
return (
<span
role="status"
aria-label="Loading"
className={`inline-flex shrink-0 flex-col items-center justify-center ${className}`}
style={{ width, height, ...style }}
>
{Array.from({ length: ROWS }).map((_, row) => (
<span key={row} aria-hidden="true" className="relative w-full" style={{ height: dot }}>
{[0, 1].map((strand) => (
<span
key={strand}
ref={(el) => {
dots.current[row * 2 + strand] = el;
}}
className="absolute top-0 block rounded-full"
style={{ width: dot, height: dot, left: `calc(50% - ${dot / 2}px)`, backgroundColor: colorMid }}
/>
))}
</span>
))}
</span>
);
}
// props
Need the license details? Read the component license.