Skip to content
goodtabs UI
BrowseBrowse components

Loading overlay

Covers one region with a spinner while it loads, never the whole page.

npx shadcn@latest add https://ui.goodtabs.co/r/loading-overlay.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/loading-overlay

Deploys

  • main, 2 minutes ago
  • fix-nav, 1 hour ago
  • release-14, yesterday
Loading

A card covered while its list loads.

Refresh

Build report

Bundle size
182 kB
First paint
0.8 s
Layout shift
0.01

A button toggles the overlay on a report for 1.5 seconds.

Usage

import { LoadingOverlay } from "@/components/ui/loading-overlay"

<LoadingOverlay loading={isPending} label="Refreshing">
  <ReportTable />
</LoadingOverlay>

Props

PropTypeDefaultDescription
loadingbooleanfalseShows the overlay, sets aria-busy and makes the content inert.
labelstring"Loading"Status text announced and shown beside the spinner.

Source

loading-overlay/loading-overlay.tsx
import * as React from "react";
import { Loader2Icon } from "lucide-react";
import { cn } from "@/lib/utils";

type LoadingOverlayProps = React.ComponentProps<"div"> & {
  /** Covers the region and marks it busy. */
  loading?: boolean;
  /** Short status text next to the spinner, for example "Loading results". */
  label?: string;
};

/**
 * Covers one region while it loads. The wrapper is aria-busy and the content
 * underneath is inert, so focus and clicks cannot reach it. It never covers
 * more than its own box, so the rest of the page keeps working.
 */
export function LoadingOverlay({
  loading = false,
  label = "Loading",
  className,
  children,
  ...props
}: LoadingOverlayProps) {
  return (
    <div
      data-slot="loading-overlay"
      data-loading={loading || undefined}
      aria-busy={loading}
      className={cn("relative", className)}
      {...props}
    >
      <div inert={loading} className="contents">
        {children}
      </div>
      {loading && (
        <div
          role="status"
          className="absolute inset-0 z-10 flex items-center justify-center gap-2 rounded-[inherit] bg-background/60 text-sm text-foreground backdrop-blur-[1px]"
        >
          <Loader2Icon className="size-4 animate-spin motion-reduce:animate-none" aria-hidden="true" />
          <span>{label}</span>
        </div>
      )}
    </div>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/loading-overlay/prompt.md, or the Copy AI prompt button above.

When to use

  • A panel, table or card refetches and the rest of the page should stay usable
  • Keeping stale content visible underneath while new data arrives
  • Any region whose size you do not want to change while loading

When not to use

  • First load of a list, where a skeleton shows the shape better
  • Blocking the whole page, which this piece deliberately cannot do

Agent notes

  • The overlay is absolute inside the wrapper, so the wrapper is relative and sized by its children
  • While loading the content is inert: no focus, no clicks, no find in page targets
  • Pass rounded and border classes on the wrapper; the overlay inherits its radius
  • The spinner stops under reduced motion and the label still shows