A segmented spinner for lightweight indeterminate loading states.
import { Spokes } from "@/components/loading-ui/spokes";
export function SpokesDemo() {pnpm dlx shadcn add @loading-ui/spokes
import { cn } from "@/lib/utils";
function Spokes({ className, ...props }: React.ComponentProps<"svg">) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={cn("animate-spin", className)}
{...props}
>
<path
d="M12 2V6M16.2 7.8L19.1 4.9M18 12H22M16.2 16.2L19.1 19.1M12 18V22M4.9 19.1L7.8 16.2M2 12H6M4.9 4.9L7.8 7.8"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export { Spokes };import { Spokes } from "@/components/spokes";<Spokes className="size-4" />Use Spokes when you want a loader that feels a bit lighter and more technical than a continuous ring. It works especially well in compact UI where a minimal silhouette is easier to read than a fuller circular stroke.
import { Spokes } from "@/components/spokes";
export function RefreshLabel({ isRefreshing }: { isRefreshing: boolean }) {
return (
<div className="inline-flex items-center gap-2 text-sm text-muted-foreground">
{isRefreshing ? <Spokes className="size-4" /> : null}
<span>{isRefreshing ? "Refreshing data" : "Up to date"}</span>
</div>
);
}import { Spokes } from "@/components/spokes";
export function SearchStatus() {
return (
<div className="flex items-center gap-2 rounded-md border px-3 py-2">
<Spokes className="size-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">Searching...</span>
</div>
);
}Like Ring, Spokes inherits currentColor.
<Spokes className="size-4 text-muted-foreground" />
<Spokes className="size-6 text-foreground" />
<Spokes className="size-8 text-primary" />Treat Spokes as a visual indicator, not a complete status message. Pair it with descriptive text or a screen-reader-only label when users need additional context.
Because the component is an SVG with simple line segments, it is easy to adapt: