TriLoader
PreviewCode
// preview
Track
#3a3a42
Color mid
#dbc2ff
Color to
#ffffff
// install
pnpmnpmyarnbun
npx shadcn@latest add "https://designpass.dev/r/TriLoader-TS-TW.json"Install the TriLoader component from DesignPass into this project by running:
npx shadcn@latest add "https://designpass.dev/r/TriLoader-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
/*!
* TriLoader, a DesignPass.dev component by Ernest Liu (ernestliu.com)
* Docs & live playground: https://designpass.dev/components/tri-loader
* MIT licensed, keep this notice in copies and adaptations.
*/
"use client";
import React, { useEffect, useId, useRef, type CSSProperties } from "react";
export interface TriLoaderProps {
/** Diameter of the spinner (px). */
size?: number;
/** Seconds for one full lap of the outline. */
speed?: number;
/** Stroke thickness (px). */
stroke?: number;
/** Length of the traveling dash as a fraction of the outline (0-1). */
strokeLength?: number;
/** Base outline color, always visible under the dash. Default is a dark
* tone, the opposite value of the bright fill, so the shape reads at rest. */
trackColor?: string;
/** Gradient start along the dash, our dim base tone. */
colorFrom?: string;
/** Gradient midpoint accent. */
colorMid?: string;
/** Gradient end, the bright tip. */
colorTo?: string;
className?: string;
style?: CSSProperties;
}
// Reuleaux triangle (constant-width curve) authored in a 50x50 box.
const PATH =
"M49.5,42.9c0-18.1-9.9-34-24.5-42.4C10.4,9,0.5,24.8,0.5,42.9c7.2,4.2,15.6,6.6,24.5,6.6S42.3,47.1,49.5,42.9z";
/**
* A Reuleaux triangle (a curve of constant width) traced by a gradient dash.
* The dash is a slice of the outline (pathLength normalized to 100) sliding on
* a single Web Animations API stroke-dashoffset loop. Its three-stop gradient
* (dim base, accent, bright tip) replaces the old opacity fade. Zero
* dependencies, honors prefers-reduced-motion.
*/
export default function TriLoader({
size = 40,
speed = 1,
stroke = 4,
strokeLength = 0.6,
trackColor = "#3a3a42",
colorFrom = "#8e8e97",
colorMid = "#dbc2ff",
colorTo = "#ffffff",
className = "",
style,
}: TriLoaderProps) {
const carRef = useRef<SVGPathElement>(null);
const gradientId = useId();
useEffect(() => {
const car = carRef.current;
if (!car) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
const travel = car.animate(
[{ strokeDashoffset: 0 }, { strokeDashoffset: 100 }],
{ duration: speed * 1000, iterations: Infinity, easing: "linear" },
);
return () => travel.cancel();
}, [speed]);
const dash = Math.min(Math.max(strokeLength, 0.01), 1) * 100;
// stroke-width is in the 50-unit path space, so scale it to render `stroke` px.
const strokeWidth = (stroke * 50) / size;
return (
<svg
role="status"
aria-label="Loading"
viewBox="0 0 50 50"
width={size}
height={size}
className={`inline-block shrink-0 overflow-visible ${className}`}
style={style}
>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stopColor={colorFrom} />
<stop offset="55%" stopColor={colorMid} />
<stop offset="100%" stopColor={colorTo} />
</linearGradient>
</defs>
<path d={PATH} fill="none" stroke={trackColor} strokeWidth={strokeWidth} strokeLinecap="round" />
<path
ref={carRef}
d={PATH}
fill="none"
stroke={`url(#${gradientId})`}
strokeWidth={strokeWidth}
strokeLinecap="round"
pathLength={100}
style={{ strokeDasharray: `${dash} ${100 - dash}`, willChange: "stroke-dashoffset" }}
/>
</svg>
);
}
// props
Need the license details? Read the component license.