Spinner

@sushindustries/ui

One ring, one border, one turn - with a visually hidden label, because a spinner with nothing to announce is just an animation.

4 min read
View as MarkdownEdit on GitHub

Examples are the tab where the component is shown doing a job, not demonstrating a prop. The API tab already lists the props.

Viewport width
Phone22remthe floor, the narrowest this promises to work at
tanstacktanstack add https://adamjurek.com/r/tanstack/spinner.jsonshadcnpnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/spinner.json

Press Compare. The frames are real viewports, so a layout that breaks at 320 breaks here too rather than in somebody's hands.

In a page

import { useState } from "react";
import { Spinner } from "@sushindustries/ui";

export function SaveButton({ onSave }: { onSave: () => Promise<void> }) {
	const [saving, setSaving] = useState(false);

	return (
		<button
			type="button"
			className="btn"
			disabled={saving}
			onClick={async () => {
				setSaving(true);
				await onSave();
				setSaving(false);
			}}
		>
			{saving ? <Spinner size={14} label="Saving" /> : "Save"}
		</button>
	);
}
tsx

What this example is not

Swapping the button's own text for the spinner, rather than showing both, is a choice this example makes - SpinnerSpinnerOne ring, one border, one turn - with a visually hidden label, because a spinner with nothing to announce is just an animation.@sushindustries/ui · content has no opinion on whether it replaces or sits beside a label; that layout is the caller's.