Przejdź do treści

Ładowanie

Szkielety, spinnery, postęp i optymistyczne aktualizacje — dobrane do tego, jak długo ludzie będą czekać i ile wiesz o tym oczekiwaniu.

Oczekiwanie wydaje się krótsze, gdy widać, że coś się dzieje, i mniej więcej wiadomo, co się pojawi. Wskaźnik dobieraj do tego, jak długo trwa oczekiwanie i ile o nim wiesz.

Członkowie zespołu

Szkielet dokładnie odwzorowuje docelowy układ, więc nic nie przeskakuje, gdy dane się załadują.

"use client"

import * as React from "react"
import { RefreshCwIcon } from "lucide-react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"

const people = [
  { initials: "AL", name: "Ada Lovelace", role: "Właściciel" },
  { initials: "GH", name: "Grace Hopper", role: "Administrator" },
  { initials: "KJ", name: "Katherine Johnson", role: "Członek" },
]

export default function LoadingSwap() {
  const [loading, setLoading] = React.useState(true)

  React.useEffect(() => {
    if (!loading) return
    const timer = setTimeout(() => setLoading(false), 1600)
    return () => clearTimeout(timer)
  }, [loading])

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <div className="flex items-center justify-between">
        <span className="text-sm font-medium">Członkowie zespołu</span>
        <Button
          variant="ghost"
          size="xs"
          onClick={() => setLoading(true)}
          loading={loading}
        >
          <RefreshCwIcon data-icon="inline-start" />
          Odśwież
        </Button>
      </div>
      <ul
        aria-busy={loading}
        className="flex flex-col divide-y rounded-xl border bg-card shadow-raised"
      >
        {people.map((person) => (
          <li key={person.name} className="flex h-14 items-center gap-3 px-4">
            {loading ? (
              <>
                <Skeleton className="size-8 rounded-full" />
                <div className="flex flex-1 flex-col gap-1.5">
                  <Skeleton className="h-3 w-32" />
                  <Skeleton className="h-2.5 w-20" />
                </div>
                <Skeleton className="h-5 w-14 rounded-md" />
              </>
            ) : (
              <>
                <Avatar>
                  <AvatarFallback>{person.initials}</AvatarFallback>
                </Avatar>
                <div className="flex flex-1 flex-col">
                  <span className="text-sm font-medium">{person.name}</span>
                  <span className="text-xs text-muted-foreground">
                    {person.name.split(" ")[0].toLowerCase()}@acme.co
                  </span>
                </div>
                <Badge variant="secondary">{person.role}</Badge>
              </>
            )}
          </li>
        ))}
      </ul>
      <p className="text-xs text-muted-foreground">
        Szkielet dokładnie odwzorowuje docelowy układ, więc nic nie przeskakuje, gdy dane się załadują.
      </p>
    </div>
  )
}

Wybór wskaźnika

OczekiwanieZnany kształt?Co pokazać
< 100 ms—Nic. Cokolwiek pokażesz, tylko mignie.
100 ms – 1 sTakSzkielet, który odwzorowuje docelowy układ.
100 ms – 1 sNieSpinner z opóźnieniem ok. 200 ms, żeby przy szybkich odpowiedziach w ogóle się nie pojawiał. Spinner sam się nie opóźnia: zamontuj go po upływie tego czasu.
1 s – 10 s—Pasek postępu, jeśli możesz zmierzyć postęp; w przeciwnym razie spinner z etykietą.
> 10 s—Postęp z szacowanym czasem — pozwól też wyjść i wrócić później, a po zakończeniu wyślij powiadomienie.

Szkielety

  • Odwzoruj docelowy układ co do piksela — te same wysokości, ten sam rytm — żeby nic się nie przesunęło, gdy pojawi się treść.
  • Oznacz ładowany obszar atrybutem aria-busy="true"; same szkielety mają aria-hidden.
  • Przy ograniczonym ruchu połysk się zatrzymuje, ale kształt zastępczy zostaje.

Przyciski

Akcje, które wymagają czasu, pokazują własny postęp dzięki Button loading: etykietę zastępuje spinner, a przycisk nie zmienia przy tym szerokości; ponowna aktywacja jest zablokowana, a fokus zostaje na miejscu.

Optymistyczne aktualizacje

Gdy akcja prawie zawsze się udaje — oznaczanie gwiazdką, archiwizowanie, zmiana kolejności — zaktualizuj interfejs od razu, a stan uzgodnij w tle. Jeśli coś się nie powiedzie, wycofaj zmianę i poinformuj o tym powiadomieniem. Optymistyczne akcje, które coś usuwają, łącz z możliwością cofnięcia, a nie z potwierdzeniem — zobacz Akcje destrukcyjne.