Skip to content
Caseroom

Components · Feedback

Empty state

Nothing here yet, or no results: a mark, what is going on, what to do, announced when it replaces results.

0.78% of the 2,062 sites in The UI Index have a empty state (16 sites).

No projects yet

Connect a repository and your first deploy starts right away.

No deploys match “pricng”

Check the spelling, or clear the filters.

Both previews render the same markup; the HTML tab below is that markup without a framework.

Install

React
npx shadcn@latest add https://www.themes.dev/caseroom/r/empty-state.json
Astro
npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/empty-state.json

Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS.

empty-state.tsx
import type { ReactNode } from 'react';
import { cn } from '@/lib/utils';
import { emptyState, MARKS } from './empty-state.variants';

export interface EmptyStateProps {
  title: string;
  description?: string;
  mark?: keyof typeof MARKS;
  /** It replaces results (a search, a filter): announce it. */
  live?: boolean;
  /** One or two actions. */
  children?: ReactNode;
  className?: string;
}

/** Nothing here yet, or no results: a mark, what is going on, what to do. */
export function EmptyState({ title, description, mark = 'inbox', live, children, className }: EmptyStateProps) {
  return (
    <div role={live ? 'status' : undefined} className={cn(emptyState.root, className)}>
      <span className={emptyState.mark} aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><path d={MARKS[mark]} /></svg></span>
      <p className={emptyState.title}>{title}</p>
      {description && <p className={emptyState.description}>{description}</p>}
      {children && <div className={emptyState.actions}>{children}</div>}
    </div>
  );
}
empty-state.variants.ts
/** An empty state: nothing here yet, or no results. A small drawn mark, one line on what is going on, one on what to
 *  do, and at most two actions. Centred on its own (it is the whole content of its area); role=status when it
 *  replaces results, so the change is announced. */
export const emptyState = {
  // h-full: side by side, the actions of a row line up at the bottom.
  root: 'mx-auto flex h-full w-full max-w-sm flex-col items-center px-6 py-10 text-center',
  mark: 'grid size-12 place-items-center rounded-full bg-muted text-muted-foreground [&_svg]:size-5',
  title: 'mt-4 text-base font-semibold text-balance',
  description: 'mt-1.5 text-sm text-pretty text-muted-foreground',
  actions: 'mt-auto flex flex-wrap justify-center gap-2 pt-5',
};
export const MARKS = {
  search: 'M7 12.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM11 11l3.5 3.5',
  inbox: 'M2 9h3.5l1 2h3l1-2H14M2 9l1.5-5.5h9L14 9v3.5H2z',
  plus: 'M8 3v10M3 8h10',
};