AngleRing
A comet travelling a card's border — a conic gradient driven by a registered @property angle, so it animates on the compositor.
Install
$ npx shadcn@latest add wisp.pouriah.com/r/angle-ring.jsonCSSno JS
Frame budget
0.41 ms
▲ 118 fps · M1 Air
"use client";
import * as React from "react";
import { cx, useTokenColorExpr } from "@pouriahlabs/wisp-ui";
/**
* AngleRing — a comet travelling a card's whole border.
*
* A conic gradient rotated by animating a registered `@property` angle. That
* registration is the point: without it, `--angle` is an untyped string and the
* browser can't interpolate it, so the whole thing falls back to a stepped
* repaint on the main thread. Typed as `<angle>`, it animates on the compositor
* and costs nothing.
*
* The ring is a padded parent whose background is the gradient; the child sits
* on top with its own surface, leaving only the padding visible as the border.
*/
export interface AngleRingProps extends React.HTMLAttributes<HTMLDivElement> {
fromToken?: string;
toToken?: string;
/** Milliseconds per revolution. */
duration?: number;
/** Border thickness, px. */
width?: number;
/** Class for the inner surface. Give it your card background. */
innerClassName?: string;
}
export function AngleRing({
fromToken = "--wisp",
toToken = "--lantern",
duration = 3400,
width = 1.5,
className,
innerClassName,
children,
style,
...props
}: AngleRingProps) {
const from = useTokenColorExpr(fromToken);
const to = useTokenColorExpr(toToken);
return (
<div
className={cx("wisp-ring", className)}
style={
{
"--wisp-ring-from": from,
"--wisp-ring-to": to,
"--wisp-ring-duration": `${duration}ms`,
"--wisp-ring-width": `${width}px`,
...style,
} as React.CSSProperties
}
{...props}
>
<div className={cx("wisp-ring-inner", innerClassName)}>{children}</div>
</div>
);
}
Certified against the contract
- ✓Reduced motion paints one composed still frame — never a blank box
- ✓Pauses off-screen and in hidden tabs
- ✓Re-reads design tokens when the theme changes
- ✓SSR-safe: no hydration mismatch, no layout shift
- ✓Decorative layers are aria-hidden and pointer-events-none
- ✓Device pixel ratio clamped
- ✓Zero runtime dependencies beyond React