Error State with Retry

An error/failed-load state inspired by Notion. Shows a typed icon, title, description, and a primary retry button with a loading state. Drop-in sibling to the Empty State pattern.

Empty StatesInspired by Notion
Live PreviewInteractive
Dashboard · Error State

Something went wrong

An unexpected error occurred. Please try again or contact support if the problem persists.

Click Try again to see the loading state · swap variant above

ErrorState.tsx
// Dependencies: react ^18, lucide-react
import React, { useState } from 'react';
import type { LucideIcon } from 'lucide-react';
import { AlertTriangle, RefreshCw } from 'lucide-react';

function cn(...classes: (string | false | null | undefined)[]) {
  return classes.filter(Boolean).join(' ');
}

type ErrorStateProps = {
  icon?: LucideIcon;
  title?: string;
  description?: string;
  primaryAction?: { label: string; onClick: () => void };
  secondaryAction?: { label: string; onClick: () => void };
  retrying?: boolean;
  size?: 'sm' | 'md' | 'lg';
};

const sizeConfig = {
  sm: {
    container: 'py-8 px-6',
    iconWrap: 'w-10 h-10 rounded-xl',
    iconSize: 'w-5 h-5',
    title: 'text-base',
    description: 'text-xs max-w-[240px]',
    primaryBtn: 'px-3 py-1.5 text-xs',
    secondaryBtn: 'px-3 py-1.5 text-xs',
  },
  md: {
    container: 'py-12 px-8',
    iconWrap: 'w-14 h-14 rounded-2xl',
    iconSize: 'w-6 h-6',
    title: 'text-lg',
    description: 'text-sm max-w-[300px]',
    primaryBtn: 'px-4 py-2 text-sm',
    secondaryBtn: 'px-4 py-2 text-sm',
  },
  lg: {
    container: 'py-20 px-10',
    iconWrap: 'w-20 h-20 rounded-3xl',
    iconSize: 'w-9 h-9',
    title: 'text-2xl',
    description: 'text-base max-w-[380px]',
    primaryBtn: 'px-5 py-2.5 text-sm',
    secondaryBtn: 'px-5 py-2.5 text-sm',
  },
};

export function ErrorState({
  icon: Icon = AlertTriangle,
  title = 'Something went wrong',
  description = 'An unexpected error occurred. Please try again.',
  primaryAction,
  secondaryAction,
  retrying = false,
  size = 'md',
}: ErrorStateProps) {
  const s = sizeConfig[size];
  return (
    <div className={cn('flex flex-col items-center text-center', s.container)}>
      <div className={cn('flex items-center justify-center mb-5 bg-red-500/10 border border-red-500/20', s.iconWrap)}>
        <Icon className={cn('text-red-400', s.iconSize)} />
      </div>
      <h3 className={cn('font-bold text-white mb-2', s.title)}>{title}</h3>
      <p className={cn('text-zinc-500 leading-relaxed mb-6', s.description)}>{description}</p>
      {(primaryAction || secondaryAction) && (
        <div className="flex items-center gap-2 flex-wrap justify-center">
          {primaryAction && (
            <button
              onClick={primaryAction.onClick}
              disabled={retrying}
              className={cn(
                'flex items-center gap-2 rounded-lg bg-indigo-500 hover:bg-indigo-400 disabled:opacity-60 disabled:cursor-not-allowed text-white font-semibold transition-colors',
                s.primaryBtn
              )}
            >
              {retrying && <RefreshCw className="w-3.5 h-3.5 animate-spin" />}
              {retrying ? 'Retrying…' : primaryAction.label}
            </button>
          )}
          {secondaryAction && (
            <button
              onClick={secondaryAction.onClick}
              disabled={retrying}
              className={cn(
                'rounded-lg border border-[#2a2a2a] text-zinc-400 hover:border-[#3a3a3a] hover:text-zinc-200 disabled:opacity-40 font-medium transition-colors',
                s.secondaryBtn
              )}
            >
              {secondaryAction.label}
            </button>
          )}
        </div>
      )}
    </div>
  );
}

// Usage — wire retrying state to a real fetch:
// function MyPage() {
//   const [retrying, setRetrying] = useState(false);
//   const retry = async () => {
//     setRetrying(true);
//     try { await fetchData(); } finally { setRetrying(false); }
//   };
//   return (
//     <ErrorState
//       title="Failed to load"
//       description="Could not fetch your projects."
//       retrying={retrying}
//       primaryAction={{ label: 'Try again', onClick: retry }}
//       secondaryAction={{ label: 'Go back', onClick: () => history.back() }}
//     />
//   );
// }