A flexible empty state component inspired by Vercel. Handles zero-data scenarios with clear messaging and actionable next steps.
Create your first project to start tracking work.
// 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