Spinner
Nieokreślony wskaźnik aktywności na krótkie oczekiwanie — gdy system pracuje, a wynik jest już w drodze.
import { Spinner } from "@/components/ui/spinner"
export default function SpinnerDemo() {
return (
<div
role="status"
className="flex items-center gap-3 rounded-full border bg-card py-2 pr-4 pl-3 text-sm text-muted-foreground shadow-raised"
>
<Spinner aria-hidden="true" label="" />
Synchronizacja 24 plików…
</div>
)
}Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/spinner.jsonUżycie
import { Spinner } from "@/components/ui/spinner"<Spinner label="Loading comments" />Spinner w prfct to dwanaście kresek, które wzdłuż okręgu stopniowo bledną — od pełnego krycia do ledwie widocznych — i obracają się w dwunastu skokach. Skokowy ruch od razu odczytuje się jako „trwa praca”, a blednące kreski nadal przypominają spinner, nawet gdy przy ograniczonym ruchu zastyga on na jednej klatce.
Przykłady
Rozmiary
Spinner ma size-4 (16 px) — rozmiar ikon w całym systemie — więc pasuje do przycisków, pól i elementów menu bez żadnych poprawek. Skaluj go klasami size-*; to SVG, więc w każdym rozmiarze pozostaje ostry.
import { Spinner } from "@/components/ui/spinner"
export default function SpinnerSizes() {
return (
<div className="flex items-center gap-6">
<Spinner className="size-3" />
<Spinner />
<Spinner className="size-5" />
<Spinner className="size-6" />
<Spinner className="size-8" />
</div>
)
}Kolory
Kreski używają currentColor. Domyślnie spinner dziedziczy kolor otaczającego tekstu; inny kolor ustawisz klasą text-* — text-muted-foreground sprawdzi się przy cichej, drugoplanowej pracy.
import { Spinner } from "@/components/ui/spinner"
export default function SpinnerColors() {
return (
<div className="flex items-center gap-6">
<Spinner className="size-5" />
<Spinner className="size-5 text-muted-foreground" />
<Spinner className="size-5 text-brand" />
<Spinner className="size-5 text-success-11" />
<span className="flex size-10 items-center justify-center rounded-lg bg-primary">
<Spinner className="size-5 text-primary-foreground" />
</span>
</div>
)
}W przyciskach
W pierwszej kolejności sięgaj po prop loading komponentu Button: zachowuje szerokość i fokus przycisku oraz ustawia aria-busy. Składaj spinner samodzielnie tylko wtedy, gdy etykieta ma się zmieniać w trakcie akcji.
import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"
export default function SpinnerInButton() {
return (
<div className="flex flex-wrap items-center justify-center gap-3">
{/* Preferred: keeps width and focus, sets aria-busy. */}
<Button loading>Zapisz zmiany</Button>
{/* Composition: when the label should change while working. */}
<Button variant="outline" disabled>
<Spinner data-icon="inline-start" aria-hidden="true" label="" />
Generowanie raportu…
</Button>
</div>
)
}<Button variant="outline" disabled>
<Spinner data-icon="inline-start" aria-hidden="true" label="" />
Generating report…
</Button>Ładowanie sekcji
W panelu, który pobiera dane, wyśrodkuj spinner i dodaj krótkie zdanie o tym, co się dzieje. Jeśli znasz układ treści, zwykle lepszym wyborem jest Skeleton.
Przeliczamy dane z zeszłego miesiąca…
import { Spinner } from "@/components/ui/spinner"
export default function SpinnerSection() {
return (
<div
aria-busy="true"
className="flex h-44 w-full max-w-md flex-col items-center justify-center gap-3 rounded-xl border border-dashed bg-surface"
>
<Spinner
className="size-5 text-muted-foreground"
label="Ładowanie analityki"
/>
<p className="text-sm text-muted-foreground">
Przeliczamy dane z zeszłego miesiąca…
</p>
</div>
)
}Wytyczne
Kiedy używać
- Gdy trwa żądanie, które ktoś sam wywołał: zapisywanie, wysyłanie, odświeżanie.
- Przy pracy w tle związanej z konkretną kontrolką lub wierszem: Synchronizowanie… obok pliku.
Kiedy nie używać
- Przy oczekiwaniu krótszym niż około sekunda. Opóźnij spinner o ~200 ms, żeby przy szybkich odpowiedziach w ogóle się nie pojawiał.
- Przy długich zadaniach z mierzalnym postępem — użyj komponentu Progress.
- Przy treści o znanym układzie — użyj komponentu Skeleton.
Jeden spinner naraz
Strona pełna spinnerów mówi: „wszystko się zepsuło”. Pokaż wskaźnik tam, gdzie skupia się uwaga — przy naciśniętym przycisku, przy zmienionym wierszu — a resztę interfejsu zostaw w pełni używalną.
Przesyłanie 3 z 12…
Dostępność
- Samodzielny spinner renderuje
role="status"zaria-labelpobranym z propalabel(domyślnie Loading albo jego tłumaczenie z LocaleProvider, po polsku Ładowanie). Nadaj mu konkretną etykietę — Ładowanie komentarzy mówi więcej niż Ładowanie. - Gdy widoczny tekst już opisuje oczekiwanie, umieść
role="status"na elemencie nadrzędnym i ukryj spinner przezaria-hidden="true", żeby komunikat nie był odczytywany dwa razy. - Wewnątrz przycisku spinner jest dekoracyjny: znaczenie niosą etykieta przycisku i
aria-busy. - Przy
prefers-reduced-motionobrót zatrzymuje się na jednej klatce. Blednące kreski sygnalizują aktywność nawet bez ruchu.
Dokumentacja API
Spinner
Renderuje <svg> z data-slot="spinner". Przyjmuje wszystkie propsy SVG.