Ł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.
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
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.