Browse all components
Error & Retry UI
What to show when a generation fails — a plain-English reason, a retry button, and a countdown for rate limits.
Preview
The request timed out while the model was generating. Your message was saved, so you can retry without retyping.
Presets
Props
Also takes onRetry.
Agent promptClaude Code · Cursor · any agent
Add the Error & Retry UI component from the Scrim UI registry to this project and use it in the configuration described below.
Error & Retry UI — What to show when a generation fails — a plain-English reason, a retry button, and a countdown for rate limits.
## 1. Install
```bash
npx shadcn@latest add @scrimui/error-message
```
This writes a single file to `components/ui/error-message.tsx`. It is plain React + Tailwind with no runtime dependencies — no Radix, no CVA, nothing to add to package.json. If this project does not use the shadcn CLI, copy the source from https://scrimui.dev/components/error-message to the same path by hand; nothing in the file depends on shadcn.
## 2. Use it
```tsx
import { ErrorMessage } from "@/components/ui/error-message";
<ErrorMessage
severity="error"
message="The request timed out while the model was generating. Your message was saved, so you can retry without retyping."
retryCountdown={0}
onRetry={() => {}}
/>
```
## 3. The configuration that matters
This is the component's "Error" state — A red-tinted message with a one-line explanation and a single retry action.
Every prop is at its default value. Keep the call site minimal — do not write out default values.
## 4. Wire the handlers
`onRetry` is a stub in the snippet above (`() => {}`). Connect it to this project's own state and data layer instead of shipping the empty function.
Reference: https://scrimui.dev/components/error-messageFollows the props below — change a control and the prompt changes with it, so an agent reproduces that configuration instead of the defaults.
Install
npx shadcn@latest add @scrimui/error-messageComponent source
React + Tailwind component using shadcn/ui (button). The install command adds these dependencies automatically.
When copying source manually, install the required primitives first:
npx shadcn@latest add button"use client";
import { Button } from "@/components/ui/button";
import * as React from "react";
/* ------------------------------------------------------------------ */
/* Types */
/* ------------------------------------------------------------------ */
export type ErrorMessageProps = {
title?: string;
message: string;
/** Show an inline retry action. */
onRetry?: () => void;
/** True while a retry is in flight — shows a spinner instead of the button. */
retrying?: boolean;
/** Seconds until retry is allowed — shows a countdown when > 0. */
retryCountdown?: number;
/** One of "error" | "rate-limit" | "warning" — changes the icon and tint. */
severity?: "error" | "rate-limit" | "warning";
className?: string;
};
/* ------------------------------------------------------------------ */
/* Icons */
/* ------------------------------------------------------------------ */
function TriangleIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="16" height="16">
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" />
<path d="M12 9v4" />
<path d="M12 17h.01" />
</svg>
);
}
function GaugeIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="16" height="16">
<path d="M12 14l4-4" />
<path d="M3.34 19a10 10 0 1 1 17.32 0" />
</svg>
);
}
function RetryIcon() {
return (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="13" height="13">
<path d="M21 12a9 9 0 1 1-2.64-6.36L21 8" />
<path d="M21 3v5h-5" />
</svg>
);
}
/* ------------------------------------------------------------------ */
/* ErrorMessage */
/* ------------------------------------------------------------------ */
export function ErrorMessage({
title,
message,
onRetry,
retrying = false,
retryCountdown = 0,
severity = "error",
className = "",
}: ErrorMessageProps) {
const isRateLimit = severity === "rate-limit";
const Icon = isRateLimit ? GaugeIcon : TriangleIcon;
const tint = isRateLimit
? "text-amber-600 dark:text-amber-400"
: "text-red-600 dark:text-red-400";
return (
<div className={`flex items-start gap-3 rounded-xl border border-red-200 bg-red-50/60 px-4 py-3 dark:border-red-900/50 dark:bg-red-950/30 ${className}`}>
<span className={`mt-0.5 shrink-0 ${tint}`}>
<Icon />
</span>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-foreground">
{title ?? (isRateLimit ? "Slow down a bit" : "Something went wrong")}
</div>
<p className="mt-0.5 text-sm leading-5 text-muted-foreground">{message}</p>
{onRetry && !retrying && retryCountdown === 0 && (
<Button variant="outline" size="sm"
type="button"
onClick={onRetry}
className="mt-2 inline-flex h-8 items-center gap-1.5 rounded-md border border-border bg-card px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-muted"
>
<RetryIcon />
Retry
</Button>
)}
{}
{retrying && (
<div className="mt-2 inline-flex h-7 items-center gap-1.5 text-xs text-muted-foreground">
<span className="h-3.5 w-3.5 animate-spin rounded-full border-2 border-border border-t-foreground " />
Retrying…
</div>
)}
{!retrying && retryCountdown > 0 && (
<div className="mt-2 text-xs text-muted-foreground">
Try again in {retryCountdown}s
</div>
)}
</div>
</div>
);
}
When to use it
- Say what happened in one plain sentence and, if possible, what was preserved — a saved draft turns a failure into a small win.
- Offer exactly one recovery path. Error and retry, or rate-limit and wait — never both.
- Show a countdown for rate limits; a timer makes waiting feel finite and the user stops hammering retry.
- Keep errors inside the message thread so the conversation context survives the failure.
What breaks in production
- A generic 'Something went wrong' with no hint of what to do next — the user has no lever.
- Auto-retrying on a loop without feedback; an invisible retry storm looks like a hang.
- Clearing the failed message or the composer on error, destroying the user's input.
Related Components
Render an AI reply token by token like ChatGPT's typing effect — blinking cursor, stop button, and no layout jump as the text grows.
The user's chat bubble — right-aligned, with hover actions to copy, edit the prompt, or regenerate the answer.
The action row under an AI reply — copy, regenerate, share and thumbs up/down feedback, revealed on hover.
A rendered markdown reply — syntax-highlighted code blocks with copy buttons, plus tables, lists and safe links.