Skip to content
Scrim UI

Error & Retry UI

What to show when a generation fails — a plain-English reason, a retry button, and a countdown for rate limits.

messageerrorretryfailure

Preview

Something went wrong

The request timed out while the model was generating. Your message was saved, so you can retry without retyping.

Presets

Props

0

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-message

Follows 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-message

Component 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
error-message.tsx
"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