caliper uidocs

Loading swap

Free

Swaps a loading placeholder for its content without a jump: the content fades in while the height eases once.

Installs components/ui/loading-swap.tsxDepends on none beyond ReactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/loading-swap.json
Loading the account summary

When to use #

Use it for

  • Panels and cards that load after the page.
  • Sections whose final height differs from the skeleton.

Skip it when

  • Whole pages; show the page frame at once instead.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/loading-swap.json

Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import { LoadingSwap } from "@/components/ui/loading-swap"
import { Skeleton } from "@/components/ui/skeleton"

export function LoadingSwapDemo() {
  const [loading, setLoading] = useState(true);
  useEffect(() => {
    if (!loading) return;
    const id = setTimeout(() => setLoading(false), 1400);
    return () => clearTimeout(id);
  }, [loading]);
  return (
    <div className="grid w-full max-w-sm gap-3">
      <div className="rounded-[12px] p-4 shadow-[inset_0_0_0_1px_var(--border)]">
        <LoadingSwap loading={loading} label="Loading the account summary" placeholder={<Skeleton lines={2} avatar label="Loading" />}>
          <div className="grid gap-2 text-sm">
            <p className="font-medium">Ostra Freight</p>
            <p className="text-muted-foreground">Four entities, two currencies. Renewal on 30 October; security review is the last open item before signature.</p>
            <p className="text-muted-foreground">Owner Maya Chen · ARR €48,000 · Health: good</p>
          </div>
        </LoadingSwap>
      </div>
      <Button size="sm" variant="secondary" className="w-fit" onClick={() => setLoading(true)} disabled={loading}>Reload</Button>
    </div>
  );
}

API reference #

LoadingSwap

component

Also accepts the native props of its root element and a ref.

PropTypeDefaultDescription
loading *boolean—
placeholder *ReactNode—What stands in while loading, e.g. a skeleton the shape of the content.
labelstring"Loading"Read to screen readers while loading.

Accessibility #

It exposes status semantics, with aria-busy and aria-hidden kept in step with state. Changes that matter are announced through a live region.

No custom key handling; Tab and the native activation keys do the work.

Motion #

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.