SquircleLoader
PreviewCode
// preview
Track
#3a3a42
Color mid
#dbc2ff
Color to
#ffffff
// install
pnpmnpmyarnbun
npx shadcn@latest add "https://designpass.dev/r/SquircleLoader-TS-TW.json"Install the SquircleLoader component from DesignPass into this project by running:
npx shadcn@latest add "https://designpass.dev/r/SquircleLoader-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
/*!
* SquircleLoader, a DesignPass.dev component by Ernest Liu (ernestliu.com)
* Docs & live playground: https://designpass.dev/components/squircle-loader
* MIT licensed, keep this notice in copies and adaptations.
*/
"use client";
import React, { useEffect, useId, useRef, type CSSProperties } from "react";
export interface SquircleLoaderProps {
/** 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;
}
// Squircle outline authored in a 50x50 box; the SVG scales it to `size`.
const PATH =
"M0.5,25C0.5,7.8,7.8,0.5,25,0.5S49.5,7.8,49.5,25S42.2,49.5,25,49.5S0.5,42.2,0.5,25";
/**
* A superellipse (squircle) outline traced by a gradient dash. The dash length
* is a slice of the path (pathLength normalized to 100) and slides with a
* single Web Animations API stroke-dashoffset loop, so the whole thing runs on
* one cheap animation. The moving segment is stroked with a three-stop gradient
* (dim base, accent, bright tip) that stands in for the old opacity fade. Zero
* dependencies, honors prefers-reduced-motion.
*/
export default function SquircleLoader({
size = 40,
speed = 1,
stroke = 4,
strokeLength = 0.6,
trackColor = "#3a3a42",
colorFrom = "#8e8e97",
colorMid = "#dbc2ff",
colorTo = "#ffffff",
className = "",
style,
}: SquircleLoaderProps) {
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.