Przejdź do treści

Spinner

Nieokreślony wskaźnik aktywności na krótkie oczekiwanie — gdy system pracuje, a wynik jest już w drodze.

Kod źródłowyRejestr
Synchronizacja 24 plików…
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.json

Uż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…

DobrzeSpinner jest tuż przy tym, co właśnie się dzieje, i ma krótki opis.
ŹleSpinner bez kontekstu każe zgadywać, na co się czeka.

Dostępność

  • Samodzielny spinner renderuje role="status" z aria-label pobranym z propa label (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 przez aria-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-motion obró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.

PropTypDomyślnie
label

Dostępna nazwa, ustawiana jako aria-label. Domyślnie komunikat loading bieżącego języka. Gdy spinner jest dekoracyjny, przekaż pusty ciąg znaków: zostanie wtedy ukryty przed technologiami wspomagającymi.

stringmessages.loading
className

Rozmiar (size-*) i kolor (text-*). Domyślnie size-4 i currentColor.

stringBrak wartości domyślnej