Spinner
Indeterminate loading indicator. Wrap children — shown when loading is false.
Examples
Badge
Add a spinner to a badge to indicate a loading state. Place the <Spinner /> before the label with data-icon='inline-start' for a start position, or after the label with data-icon='inline-end' for an end position.
import { Badge } from "@codefast/ui/badge";
import { Spinner } from "@codefast/ui/spinner";
export function SpinnerBadge() {
return (
<div className="flex items-center gap-4 [--radius:1.2rem]">
<Badge>
<Spinner data-icon="inline-start" />
Syncing
</Badge>
<Badge variant="secondary">
<Spinner data-icon="inline-start" />
Updating
</Badge>
<Badge variant="outline">
<Spinner data-icon="inline-start" />
Processing
</Badge>
</div>
);
}
Customization
Replace the default spinner icon with any other icon.
import { cn } from "@codefast/ui/lib/utils";
import { LoaderIcon } from "lucide-react";
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return <LoaderIcon role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} />;
}
export function SpinnerCustom() {
return (
<div className="flex items-center gap-4">
<Spinner />
</div>
);
}
Empty
An indicator that can be used to show a loading state.
Please wait while we process your request. Do not refresh the page.
import { Button } from "@codefast/ui/button";
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@codefast/ui/empty";
import { Spinner } from "@codefast/ui/spinner";
export function SpinnerEmpty() {
return (
<Empty className="w-full">
<EmptyHeader>
<EmptyMedia variant="icon">
<Spinner />
</EmptyMedia>
<EmptyTitle>Processing your request</EmptyTitle>
<EmptyDescription>Please wait while we process your request. Do not refresh the page.</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" size="sm">
Cancel
</Button>
</EmptyContent>
</Empty>
);
}
Input Group
An indicator that can be used to show a loading state.
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupTextarea,
} from "@codefast/ui/input-group";
import { Spinner } from "@codefast/ui/spinner";
import { ArrowUpIcon } from "lucide-react";
export function SpinnerInputGroup() {
return (
<div className="flex w-full max-w-md flex-col gap-4">
<InputGroup>
<InputGroupInput placeholder="Send a message..." disabled />
<InputGroupAddon align="inline-end">
<Spinner />
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea placeholder="Send a message..." disabled />
<InputGroupAddon align="block-end">
<Spinner /> Validating...
<InputGroupButton className="ms-auto" variant="default">
<ArrowUpIcon />
<span className="sr-only">Send</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
);
}
RTL
Right-to-left layout support for languages such as Arabic and Hebrew.
Translations are AI-generated for demonstration and may be imperfect.
import { Item, ItemContent, ItemMedia, ItemTitle } from "@codefast/ui/item";
import { Spinner } from "@codefast/ui/spinner";
import type { Translations } from "#/features/components-catalog/components/detail/language";
import { useTranslation } from "#/features/components-catalog/components/detail/language-context";
const translations: Translations = {
en: {
dir: "ltr",
values: {
title: "Processing payment...",
amount: "$100.00",
},
},
ar: {
dir: "rtl",
values: {
title: "جاري معالجة الدفع...",
amount: "١٠٠.٠٠ دولار",
},
},
he: {
dir: "rtl",
values: {
title: "מעבד תשלום...",
amount: "$100.00",
},
},
};
export function SpinnerRtl() {
const { dir, t } = useTranslation(translations, "ar");
return (
<div className="flex w-full max-w-xs flex-col gap-4 [--radius:1rem]" dir={dir}>
<Item variant="muted" dir={dir}>
<ItemMedia>
<Spinner />
</ItemMedia>
<ItemContent>
<ItemTitle className="line-clamp-1">{t.title}</ItemTitle>
</ItemContent>
<ItemContent className="flex-none justify-end">
<span className="text-sm tabular-nums">{t.amount}</span>
</ItemContent>
</Item>
</div>
);
}
Size
Use the size-* utility class to change the size of the spinner.
import { Spinner } from "@codefast/ui/spinner";
export function SpinnerSize() {
return (
<div className="flex items-center gap-6">
<Spinner className="size-3" />
<Spinner className="size-4" />
<Spinner className="size-6" />
<Spinner className="size-8" />
</div>
);
}
Usage
The minimal import and composition — see Examples below for styled, real-world variants.
import { Spinner } from "@codefast/ui/spinner";
export function SpinnerUsage() {
return <Spinner />;
}
Anatomy
How the parts nest — every slot the component exposes, in composition order.
Features
- With children and loading=true, it hides them invisibly (keeping their layout size) and overlays the spinner on top — a VisuallyHidden copy keeps the label announced to screen readers.
- loading={false} renders the children directly in place of the spinner, so a single component can toggle between the two states.
- Respects prefers-reduced-motion — the animation pauses automatically.
API reference
Props for each part of the component. All native element props are also forwarded.
Spinner
An indeterminate loading indicator.
loadingbooleanWhen false, renders children instead of the spinner.
Default
truechildrenReactNodeUsed as a visually-hidden label while spinning.
classNamestringSet the size (e.g. size-5) and colour.
Accessibility
Built to be keyboard-navigable and screen-reader friendly out of the box.
- Give the spinner a label via children so its purpose is announced.
- Prefer a Spinner over a fake progress bar when you don’t know the duration.
- Respect reduced motion — the animation is paused under prefers-reduced-motion.
Guidelines
Conventions that keep usage consistent across an app.
Do
- Use for short, indeterminate waits.
- Pair with disabled controls while an action is in flight.
Don’t
- Don’t use a spinner for long, measurable tasks — use Progress.
- Don’t show a bare spinner with no context about what’s loading.
Explore further
Ready to integrate?
Follow the Getting Started guide to install @codefast/ui, or browse the full component gallery.