Pending
A utility component that disables interactions, maintains keyboard focus, and ensures proper accessibility for buttons, forms, links, switches, and any interactive element while they are pending.
"use client";
import { Loader2 } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { usePending } from "@/components/components/pending";
export function PendingDemo() {
const [isSubmitting, setIsSubmitting] = React.useState(false);
const { pendingProps, isPending } = usePending({ isPending: isSubmitting });
const onSubmit = React.useCallback(() => {
setIsSubmitting(true);
// Simulate API call
setTimeout(() => {
setIsSubmitting(false);
}, 2000);
}, []);
return (
<div className="flex flex-col items-center gap-4">
<Button onClick={onSubmit} {...pendingProps}>
{isPending && <Loader2 className="size-4 animate-spin" />}
{isPending ? "Submitting..." : "Submit"}
</Button>
<p className="text-muted-foreground text-sm">
{isPending
? "Button is pending - try tabbing to it and pressing Enter"
: "Click the button to see pending state"}
</p>
</div>
);
}Installation
CLI
npx shadcn@latest add @diceui/pendingManual
Copy and paste the following code into your project.
/**
* Based on React Aria's Button implementation
* @see https://github.com/adobe/react-spectrum/blob/main/packages/react-aria-components/src/Button.tsx
*/
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
interface UsePendingOptions {
id?: string;
isPending?: boolean;
disabled?: boolean;
}
interface UsePendingReturn<T extends HTMLElement = HTMLElement> {
pendingProps: React.HTMLAttributes<T> & {
"aria-busy"?: "true";
"aria-disabled"?: "true";
"data-pending"?: true;
"data-disabled"?: true;
};
isPending: boolean;
}
function usePending<T extends HTMLElement = HTMLElement>(
options: UsePendingOptions = {},
): UsePendingReturn<T> {
const { id, isPending = false, disabled = false } = options;
const instanceId = React.useId();
const pendingId = id || instanceId;
const pendingProps = React.useMemo(() => {
const props: React.HTMLAttributes<T> & {
"aria-busy"?: "true";
"aria-disabled"?: "true";
"data-pending"?: true;
"data-disabled"?: true;
} = {
id: pendingId,
};
if (isPending) {
props["aria-busy"] = "true";
props["aria-disabled"] = "true";
props["data-pending"] = true;
function onEventPrevent(event: React.SyntheticEvent) {
event.preventDefault();
}
function onKeyEventPrevent(event: React.KeyboardEvent<T>) {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
}
}
props.onClick = onEventPrevent;
props.onPointerDown = onEventPrevent;
props.onPointerUp = onEventPrevent;
props.onMouseDown = onEventPrevent;
props.onMouseUp = onEventPrevent;
props.onKeyDown = onKeyEventPrevent;
props.onKeyUp = onKeyEventPrevent;
}
if (disabled) {
props["data-disabled"] = true;
}
return props;
}, [isPending, disabled, pendingId]);
return React.useMemo(() => {
return {
pendingProps,
isPending,
};
}, [pendingProps, isPending]);
}
interface PendingProps extends React.ComponentProps<typeof Slot> {
isPending?: boolean;
disabled?: boolean;
}
function Pending({ id, isPending, disabled, ...props }: PendingProps) {
const { pendingProps } = usePending({ id, isPending, disabled });
return <Slot {...props} {...pendingProps} />;
}
export {
Pending,
//
usePending,
};Layout
Import the utility and use it with your interactive elements.
import { usePending, Pending } from "@/components/pending";
import { Button } from "@/components/ui/button";
// Using the hook
function SubmitButton() {
const [isSubmitting, setIsSubmitting] = useState(false);
const { pendingProps, isPending } = usePending({ isPending: isSubmitting });
return <Button {...pendingProps}>Submit</Button>;
}
// Using the wrapper component
function SubmitButton() {
const [isSubmitting, setIsSubmitting] = useState(false);
return (
<Pending isPending={isSubmitting}>
<Button>Submit</Button>
</Pending>
);
}Examples
Wrapper Component
Use the Pending wrapper component to easily apply pending state to any interactive element using Radix Slot.
"use client";
import { Loader2 } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Pending } from "@/components/components/pending";
export function PendingWrapperDemo() {
const [isSubmitting, setIsSubmitting] = React.useState(false);
const onSubmit = React.useCallback(() => {
setIsSubmitting(true);
// Simulate API call
setTimeout(() => {
setIsSubmitting(false);
}, 2000);
}, []);
return (
<div className="flex flex-col items-center gap-4">
<Pending isPending={isSubmitting}>
<Button onClick={onSubmit}>
{isSubmitting && <Loader2 className="size-4 animate-spin" />}
{isSubmitting ? "Submitting..." : "Submit with Wrapper"}
</Button>
</Pending>
<p className="text-muted-foreground text-sm">
Using the <code className="text-xs">{"<Pending>"}</code> wrapper
component
</p>
</div>
);
}Form with Pending State
Handle form submissions with proper pending state management and user feedback.
"use client";
import { Loader2 } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { usePending } from "@/components/components/pending";
export function PendingFormDemo() {
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [submitted, setSubmitted] = React.useState(false);
const { pendingProps, isPending } = usePending({ isPending: isSubmitting });
const onSubmit = React.useCallback((event: React.FormEvent) => {
event.preventDefault();
setIsSubmitting(true);
setSubmitted(false);
// Simulate API call
setTimeout(() => {
setIsSubmitting(false);
setSubmitted(true);
setTimeout(() => setSubmitted(false), 2000);
}, 2000);
}, []);
return (
<form onSubmit={onSubmit} className="w-full max-w-sm space-y-4">
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="[email protected]"
required
disabled={isPending}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
required
disabled={isPending}
/>
</div>
<Button type="submit" className="w-full" {...pendingProps}>
{isPending && <Loader2 className="size-4 animate-spin" />}
{isPending ? "Signing in..." : "Sign in"}
</Button>
{submitted && (
<p className="text-center text-green-600 text-sm dark:text-green-400">
Successfully signed in!
</p>
)}
</form>
);
}Navigation Links
Apply pending states to links during async navigation or route transitions.
"use client";
import * as React from "react";
import { Pending } from "@/components/components/pending";
export function PendingLinkDemo() {
const [isPending, setIsPending] = React.useState(false);
const onNavigate = React.useCallback(
(event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
setIsPending(true);
// Simulate async navigation
setTimeout(() => {
setIsPending(false);
}, 2000);
},
[],
);
return (
<div className="flex items-center justify-center">
<Pending isPending={isPending}>
<a
href="/docs/components/pending"
onClick={onNavigate}
className="text-primary underline-offset-4 hover:underline"
>
{isPending ? "Loading dashboard..." : "Go to Dashboard"}
</a>
</Pending>
</div>
);
}Toggle Switches
Show pending states on switches that save settings to an API or perform async updates.
"use client";
import * as React from "react";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Pending } from "@/components/components/pending";
export function PendingSwitchDemo() {
const [isEnabled, setIsEnabled] = React.useState(false);
const [isPending, setIsPending] = React.useState(false);
const onToggle = React.useCallback(() => {
setIsPending(true);
// Simulate async API call to save setting
setTimeout(() => {
setIsEnabled((prev) => !prev);
setIsPending(false);
}, 1500);
}, []);
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<Label htmlFor="notifications">Email Notifications</Label>
<p className="text-muted-foreground text-sm">
{isPending
? "Saving..."
: "Receive email about your account activity"}
</p>
</div>
<Pending isPending={isPending}>
<Switch
id="notifications"
checked={isEnabled}
onCheckedChange={onToggle}
className="data-pending:cursor-wait data-pending:opacity-70"
/>
</Pending>
</div>
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<Label htmlFor="marketing">Marketing Updates</Label>
<p className="text-muted-foreground text-sm">
Get tips, updates, and special offers
</p>
</div>
<Switch id="marketing" />
</div>
</div>
);
}API Reference
usePending
A hook that manages pending state for interactive elements. Returns props to spread on your element and the current pending state.
Prop
Type
Returns
Prop
Type
Pending
A wrapper component that applies pending state behavior to its child using Radix Slot.
Prop
Type
| Data Attribute | Value |
|---|---|
[data-pending] | "true" |
[data-disabled] | "true" |
Accessibility
The Pending utility follows best practices for accessible pending states:
- Uses
aria-busy="true"to indicate loading/pending state to screen readers - Uses
aria-disabled="true"to indicate the element is not currently interactive - Maintains focus ability—users can still Tab to the element
- Prevents all interaction events (click, pointer, keyboard) when pending
- Screen readers announce the changed button content (e.g., "Submitting...")
- Provides
data-pendingattribute for custom styling
Use Cases
The Pending utility works with any interactive element, not just buttons:
- Buttons - Form submissions, actions
- Links - Navigation with async route transitions
- Cards - Clickable cards that load content
- Menu Items - Actions like export, sync, archive
- Switches/Toggles - Settings that save to an API
- Form Fields - Inputs with async validation
- Tabs - Tab switches that load content
- Select Options - Dropdowns with async actions
- Icon Buttons - Icon-only interactive elements
- List Items - Navigation items in sidebars
Notes
- Choose your API: Use the
usePendinghook for more control, or thePendingwrapper for convenience - Focus management: Elements remain focusable but don't respond to interactions when pending
- Event prevention: All pointer and keyboard events are prevented during pending state
- Styling: Use the
data-pendingattribute to style elements based on their pending state - Accessibility: Screen readers announce both
aria-busy(loading state) andaria-disabled(not interactive) - Hook usage: When using the hook directly, spread
pendingPropslast to ensure event prevention works correctly
Credits
- React Aria's Button component - For the pending state management patterns (Apache License 2.0).