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.
npx shadcn@latest add https://webberui.com/r/async-state-slot.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<AsyncStateSlot />
Installation
npx shadcn@latest add https://webberui.com/r/async-state-slot.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
state | "idle" | "loading" | "empty" | "error" | "content" | — | The current state (controlled) |
children | ReactNode | — | Content for the content state |
idle | ReactNode | built-in | Override for the idle state |
loading | ReactNode | built-in skeleton | Override for the loading state |
empty | ReactNode | built-in illustration | Override for the empty state |
error | ReactNode | built-in illustration | Override for the error state |
idleTitle | string | "Standing by" | Title of the built-in idle state |
idleDescription | string | "Waiting for a load to be triggered." | Description of the built-in idle state |
emptyTitle | string | "No data" | Title of the built-in empty state |
emptyDescription | string | "There is nothing here yet." | Description of the built-in empty state |
errorTitle | string | "Failed to load" | Title of the built-in error state |
errorDescription | string | "Something went wrong, please try again later." | Description of the built-in error state |
onRetry | () => void | — | When provided, shows a retry button in the built-in error state |
retryLabel | string | "Retry" | Text on the retry button |
duration | number | 0.4 | Duration of the height morph and the fade in / fade out (seconds) |
transition | Transition | — | Overrides the height morph transition (takes precedence over duration) |
minHeight | number | string | — | Minimum slot height, to keep it from collapsing while switching |
srLabels | Partial<Record<AsyncState, string>> | built-in | Overrides 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 useslayout="position"to cancel out the scaling so text is never stretched. - Shared-element transitions:
AnimatePresenceruns inmode="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:
stateis 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/errorshare one centered illustration layout, andloadingis a skeleton of an avatar plus body rows.
Accessibility
- The container carries
aria-busy(truewhileloading) anddata-state, which makes it easy for styling and assistive technology to identify. - It includes a hidden
role="status"region witharia-live="polite"that announces the current state when it changes; the text can be overridden withsrLabels. - 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.
Pull To Refresh (React)
A mobile pull-to-refresh container for React: damped drag with a threshold, an arrow that rotates into a spinner, onRefresh returning a Promise, a check on completion, plus a desktop button fallback.
Optimistic Mutation Frame
An optimistic update container that inserts a translucent ghost item immediately, solidifies it into a real one when the server confirms, and shakes it back out on failure while handing the error off to a toast.