WebberUI

Async State Slot

An async state slot that switches between the five idle/loading/empty/error/content states with height morphing and shared-element transitions.

Loading preview…
npx shadcn@latest add https://webberui.com/r/async-state-slot.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

0.4
160
<AsyncStateSlot />

Installation

npx shadcn@latest add https://webberui.com/r/async-state-slot.json

Or, once registries are configured in components.json, install it as @webberui/async-state-slot.

Usage

A single state prop drives the whole slot; when the state changes, the frame height tweens smoothly while the exiting element folds away and the entering element slides in to take its place. The content state shows children, and the other four states render a built-in placeholder unless you provide an override.

import { AsyncStateSlot } from "@/components/ui/async-state-slot";

function Inbox() {
  const [state, setState] = React.useState<AsyncState>("loading");

  React.useEffect(() => {
    fetch("/api/items")
      .then((r) => r.json())
      .then((items) => setState(items.length ? "content" : "empty"))
      .catch(() => setState("error"));
  }, []);

  return (
    <AsyncStateSlot state={state} onRetry={() => setState("loading")}>
      <ItemList />
    </AsyncStateSlot>
  );
}

Every state's built-in view can be replaced entirely through its matching prop:

<AsyncStateSlot
  state={state}
  loading={<MySkeleton />}
  empty={<MyEmptyState />}
>
  <ItemList />
</AsyncStateSlot>

Props

PropTypeDefaultDescription
state"idle" | "loading" | "empty" | "error" | "content"The current state (controlled)
childrenReactNodeContent for the content state
idleReactNodebuilt-inOverride for the idle state
loadingReactNodebuilt-in skeletonOverride for the loading state
emptyReactNodebuilt-in illustrationOverride for the empty state
errorReactNodebuilt-in illustrationOverride for the error state
idleTitlestring"Standing by"Title of the built-in idle state
idleDescriptionstring"Waiting for a load to be triggered."Description of the built-in idle state
emptyTitlestring"No data"Title of the built-in empty state
emptyDescriptionstring"There is nothing here yet."Description of the built-in empty state
errorTitlestring"Failed to load"Title of the built-in error state
errorDescriptionstring"Something went wrong, please try again later."Description of the built-in error state
onRetry() => voidWhen provided, shows a retry button in the built-in error state
retryLabelstring"Retry"Text on the retry button
durationnumber0.4Duration of the height morph and the fade in / fade out (seconds)
transitionTransitionOverrides the height morph transition (takes precedence over duration)
minHeightnumber | stringMinimum slot height, to keep it from collapsing while switching
srLabelsPartial<Record<AsyncState, string>>built-inOverrides the announced text for each state

How it works

  • Height morphing: the frame tweens smoothly to the entering element's actual height using Motion's layout, while the inner layer uses layout="position" to cancel out the scaling so text is never stretched.
  • Shared-element transitions: AnimatePresence runs in mode="popLayout", so the exiting element leaves the layout flow and the incoming element decides the new height on its own, with the two transitions overlapping — producing the "empty state folds away as the first row slides in to take its place" effect.
  • Controlled state: state is driven by a single external source and the component holds no internal state machine, which makes it easy to hook up to any async data flow.
  • Without overrides, idle / empty / error share one centered illustration layout, and loading is a skeleton of an avatar plus body rows.

Accessibility

  • The container carries aria-busy (true while loading) and data-state, which makes it easy for styling and assistive technology to identify.
  • It includes a hidden role="status" region with aria-live="polite" that announces the current state when it changes; the text can be overridden with srLabels.
  • The retry button has keyboard focus styling (focus-visible).
  • When the user has "reduce motion" enabled at the system level, the height and displacement animations are disabled and the state switches instantly.

On this page