Skip to content

Loading

Skeletons, spinners, progress and optimistic updates, chosen by how long people will wait and how much you know.

Waiting feels shorter when people can see that something is happening and roughly what will appear. Choose the indicator by how long the wait is and how much you know about it.

Team members

The skeleton matches the final layout exactly, so nothing shifts when data arrives.

"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: "Owner" },
  { initials: "GH", name: "Grace Hopper", role: "Admin" },
  { initials: "KJ", name: "Katherine Johnson", role: "Member" },
]

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">Team members</span>
        <Button
          variant="ghost"
          size="xs"
          onClick={() => setLoading(true)}
          loading={loading}
        >
          <RefreshCwIcon data-icon="inline-start" />
          Reload
        </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">
        The skeleton matches the final layout exactly, so nothing shifts when
        data arrives.
      </p>
    </div>
  )
}

Choosing an indicator

WaitKnown shape?Use
< 100ms—Nothing. Anything shown would flash.
100ms – 1sYesA skeleton that matches the final layout.
100ms – 1sNoA spinner, delayed ~200ms so fast responses never show it. Spinner has no delay of its own: mount it after a timeout.
1s – 10s—A progress bar if you can measure progress; otherwise a spinner with a label.
> 10s—Progress with an estimate, and let people leave and come back — notify them when done.

Skeletons

  • Match the final layout exactly — same heights, same rhythm — so nothing shifts when content arrives.
  • Mark the loading region with aria-busy="true"; skeletons themselves are aria-hidden.
  • The shimmer stops with reduced motion; the placeholder shape remains.

Buttons

Actions that take time show their own progress with Button loading: the label is replaced by a spinner without the button changing width, activation is blocked, and focus stays put.

Optimistic updates

When an action almost always succeeds — starring, archiving, reordering — update the interface immediately and reconcile in the background. If it fails, roll back and say so with a toast. Pair destructive optimistic actions with undo rather than confirmation; see Destructive actions.