Empty State

A flexible empty state component inspired by Vercel. Handles zero-data scenarios with clear messaging and actionable next steps.

Empty StatesInspired by Vercel
Live PreviewInteractive

No projects yet

Create your first project to start tracking work.

EmptyState.tsx
// Dependencies: react ^18, lucide-react
import React from 'react';
import type { LucideIcon } from 'lucide-react';
import { FolderOpen } from 'lucide-react';

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

type EmptyStateProps = {
  icon: LucideIcon;
  title: string;
  description: string;
  primaryAction?: { label: string; onClick: () => void };
  secondaryAction?: { label: string; onClick: () => void };
  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 EmptyState({
  icon: Icon,
  title,
  description,
  primaryAction,
  secondaryAction,
  size = 'md',
}: EmptyStateProps) {
  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 bg-zinc-900 border border-white/[0.08] mb-5', s.iconWrap)}>
        <Icon className={cn('text-zinc-500', 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}
              className={cn('rounded-lg bg-indigo-500 hover:bg-indigo-400 text-white font-semibold transition-colors', s.primaryBtn)}
            >
              {primaryAction.label}
            </button>
          )}
          {secondaryAction && (
            <button
              onClick={secondaryAction.onClick}
              className={cn('rounded-lg border border-[#2a2a2a] text-zinc-400 hover:border-[#3a3a3a] hover:text-zinc-200 font-medium transition-colors', s.secondaryBtn)}
            >
              {secondaryAction.label}
            </button>
          )}
        </div>
      )}
    </div>
  );
}

// Usage examples
// <EmptyState icon={FolderOpen} title="No projects yet" description="Create your first project." primaryAction={{ label: 'New Project', onClick: () => {} }} />
// <EmptyState icon={FolderOpen} title="No results" description="Try a different search." size="sm" />

Unlock to copy

Free access to all patterns